ES6新增了哪些特性
ES6(ECMAScript 2015)引入了许多新特性,主要包括以下几个方面:
1. 变量和常量声明
let
和const
(块级作用域,避免var
的提升问题)const
声明的变量不可重新赋值
2. 模板字符串
- 反引号 “
包裹的字符串,可嵌入变量和表达式(
${}` 语法)
3. 箭头函数
- 简化函数表达式 (
()=>{}
),不绑定this
,继承外层作用域的this
4. 函数的默认参数
- 允许在函数定义时为参数指定默认值
5. 解构赋值
- 数组和对象的解构赋值,例如:
1let [a, b] = [1, 2];
2let {name, age} = {name: "Tom", age: 25};
6. 扩展运算符(**...**
)
- 剩余参数:
function sum(...args) {}
- 展开数组/对象:
let arr2 = [...arr1]
7. **Map**
和 **Set**
结构
Set
(不重复的值集合)和Map
(键值对集合)
8. **for...of**
循环
- 适用于可迭代对象(如数组、
Map
、Set
)
**9. ****Promise**
(异步编程)
Promise
提供resolve
和reject
处理异步操作
10. **class**
关键字(类与继承)
- 更简洁的面向对象编程方式,支持
constructor
、extends
、super
11. **Symbol**
类型
- 创建唯一标识符,避免对象属性名冲突
12. **Module**
模块化
export
/import
语法实现模块化
1export function add(x, y) { return x + y; }
2import { add } from './math.js';
**13. ****Object.assign()**
- 浅拷贝对象属性
**14. **Proxy**
& ****Reflect**
(元编程)
Proxy
用于拦截对象操作Reflect
提供操作对象的默认行为
无感刷新token
当登录或刷新token时,后端都会返回两个值:token 和 refreshToken
实现流程
-
在请求开始时把请求条件(URL,入参,请求方式等)临时存储
-
请求返回:正确,则正常返回,释放请求条件临时存储
-
请求返回:错误且错误码是token过期,则把这个请求执行函数放入订阅中,然后请求refreshToken API 刷新token的请求
-
请求request时,当前如果是正在刷新token,则请求执行函数放入订阅中
-
刷新token的请求返回后,替换新的token,触发发布所有订阅中的请求函数,达到无感刷新token
VUE项目打包怎么更改目录
node怎么操作文件
Promise的了解以及new一个promise直接抛出错误,then和catch会怎么
1. Promise 的基本概念
- Promise 是一种用于表示 异步操作 的结果的对象,它可以是 成功 或 失败。
- Promise 有三种状态:
- pending(待定):初始状态,既没有成功也没有失败。
- fulfilled(已完成):操作成功完成。
- rejected(已拒绝):操作失败。
Promise 可以链式调用,并且使用 then
和 catch
来处理操作结果或错误。
2. **new Promise**
直接抛出错误的行为
当我们 new Promise 时,如果 立即抛出错误(在 executor 函数中),会导致 Promise 直接进入 rejected 状态。
代码示例:
1const promise = new Promise((resolve, reject) => {
2 throw new Error('Something went wrong!');
3});
4
5promise.then(() => {
6 console.log('This will not be executed');
7}).catch((error) => {
8 console.log('Caught error:', error.message); // "Caught error: Something went wrong!"
9});
3. **then**
和 **catch**
如何处理
**then**
:then
用于处理成功(fulfilled)和失败(rejected)结果。即使在new Promise
中抛出了错误,then
也会被跳过,错误会直接被catch
捕获。**catch**
:catch
会捕获到 Promise 的异常,包括throw
抛出的错误。
在上面的例子中,Promise 会立即进入 rejected 状态,catch
会处理抛出的错误信息。
总结:
“当我们在 **new Promise**
中直接抛出错误时,Promise 会进入 **rejected**
状态,**then**
** 不会执行成功回调,而会跳过,错误会被 **catch**
捕获并处理。“**
Vue2和Vue3响应式原理的区别
1. Vue2 响应式原理
Vue2 的响应式系统基于 Object.defineProperty,它通过劫持对象的 getter 和 setter 方法来实现数据的双向绑定和依赖追踪。
工作原理:
- Vue2 在数据对象的每个属性上都使用
**Object.defineProperty**
创建 getter 和 setter,来观察数据的变化。 - getter 用来收集依赖,即当数据被访问时,Vue 会记录当前的依赖(通常是组件的视图)。
- setter 用来触发视图更新,即当数据被修改时,Vue 会通知视图重新渲染。
- Vue2 会在数据初始化时递归地为每个嵌套对象属性都加上 getter 和 setter。
限制:
- 性能问题:Vue2 的递归遍历所有嵌套对象来定义 getter 和 setter,对于深层嵌套的对象,性能开销较大。
- 无法监听属性的新增和删除:Vue2 无法检测到对象属性的新增或删除,只能监听已经定义的属性。
2. Vue3 响应式原理
Vue3 使用了 Proxy 来替代 Vue2 的 Object.defineProperty
,从而解决了 Vue2 的一些问题,提升了性能,并扩展了响应式的功能。
工作原理:
- Vue3 使用 Proxy 来劫持对象的操作(包括 get、set、deleteProperty 等),可以动态地代理对象。
- Proxy 可以直接拦截所有的对象操作,而不仅仅是访问属性(getter)和修改属性(setter),它更加灵活。
- Vue3 通过 Proxy 实现了 更精细的依赖追踪,可以更加高效地处理深层嵌套的对象,并且不会递归地遍历每个属性。
- Vue3 在依赖追踪方面引入了 响应式代理对象,它会自动处理 依赖收集 和 更新通知。
优势:
- 性能提升:因为 Proxy 是基于对象本身的劫持,而不是逐个属性的劫持,所以 Vue3 在处理嵌套对象时,比 Vue2 更高效。
- 支持新增属性和删除属性的响应式:Vue3 可以直接检测到对象属性的新增和删除,这是 Vue2 做不到的。
- 更加灵活的响应式系统:Proxy 的 API 更加强大,可以实现更多细粒度的控制。
3. Vue2 和 Vue3 响应式系统的区别总结:
特性 | Vue2 | Vue3 |
---|---|---|
响应式实现方式 | 基于 Object.defineProperty | 基于 Proxy |
性能 | 在处理深层嵌套时性能较差 | 性能优化,支持深层嵌套时更加高效 |
新增/删除属性的响应式 | 无法监听属性的新增和删除 | 可以监听属性的新增、删除和修改 |
依赖追踪 | 依赖收集较为简单,通过 getter/setter 实现 | 依赖收集更加精细,使用 Proxy 拦截更多操作 |
限制 | 无法代理不存在的属性 | 可以代理所有对象操作,支持动态对象修改 |
总结:
“Vue2 使用 **Object.defineProperty**
来实现响应式,性能相对较差,并且不能监听新增和删除的属性。而 Vue3 则采用了 **Proxy**
,它提供了更强大和高效的响应式能力,不仅支持深层嵌套,也支持动态的属性操作(新增、删除等)。”
CSS有哪些布局
1. 常见的 CSS 布局方式
1.1. 普通文档流布局 (Normal Flow)
- 默认布局方式,元素按文档的顺序从上到下、从左到右依次排列。
- 元素的布局不会受其他元素的影响,除非使用了定位或浮动。
1<div>元素1</div>
2<div>元素2</div>
3<div>元素3</div>
这段代码将三个 <div>
元素垂直排列。
1.2. 浮动布局 (Float)
- 使用
float
属性,将元素浮动到左侧或右侧,其他元素环绕浮动元素排列。 - 常用于多列布局,或者文本环绕效果。
- 在现代布局中,浮动布局已经被 Flexbox 和 Grid 布局取代,但在老的布局中仍有使用。
1.left {
2 float: left;
3 width: 50%;
4}
5.right {
6 float: right;
7 width: 50%;
8}
1.3. 定位布局 (Position)
- 使用
position
属性来控制元素的位置。常见的值有static
、relative
、absolute
和fixed
。static
:默认值,元素按正常文档流排列。relative
:相对于元素的正常位置进行偏移。absolute
:相对于最近的已定位父元素进行定位。fixed
:相对于浏览器窗口进行定位。
1.container {
2 position: relative;
3}
4.box {
5 position: absolute;
6 top: 0;
7 right: 0;
8}
1.4. Flexbox 布局
- Flexbox 是一种一维布局方式,用于在容器内分布和对齐元素,适用于单一方向(横向或纵向)的布局。
- 主要涉及
display: flex
或display: inline-flex
,以及其他配套属性(如justify-content
、align-items
等)。
1.container {
2 display: flex;
3 justify-content: space-between;
4}
- Flexbox 可以让元素在容器中自动伸缩、对齐,并具有更好的响应式布局能力。
1.5. CSS Grid 布局
- Grid 布局是一种二维布局方式,允许在行和列上同时进行布局。它比 Flexbox 更强大,可以同时控制横向和纵向的布局。
- 通过
display: grid
来启用 Grid 布局,并用grid-template-rows
和grid-template-columns
等属性来定义网格的结构。
1.container {
2 display: grid;
3 grid-template-columns: repeat(3, 1fr);
4 gap: 10px;
5}
- Grid 布局特别适用于复杂的网页布局,尤其是需要同时处理多行和多列时。
1.6. 表格布局 (Table Layout)
- 使用
display: table
、display: table-row
和display: table-cell
来模拟表格布局。 - 适用于表格形式的布局,但在现代开发中,表格布局逐渐被 Flexbox 和 Grid 取代。
1.container {
2 display: table;
3}
4.row {
5 display: table-row;
6}
7.cell {
8 display: table-cell;
9}
1.7. 多列布局 (Multi-column Layout)
- 使用
column-count
和column-gap
等属性创建多列布局,适合文本内容的分栏显示。 - 多列布局适用于需要将内容分成多个并排显示的场景,比如新闻网站的文本内容。
1.container {
2 column-count: 3;
3 column-gap: 20px;
4}
1.8. 垂直居中
- 垂直居中常见的实现方式有多种,以下是两种常用的方法:
- Flexbox 实现垂直居中:
1.container {
2 display: flex;
3 justify-content: center;
4 align-items: center;
5}
- 绝对定位实现垂直居中:
1.container {
2 position: relative;
3}
4.box {
5 position: absolute;
6 top: 50%;
7 left: 50%;
8 transform: translate(-50%, -50%);
9}
2. 总结:
“CSS 布局有多种方式,常见的包括普通文档流布局、浮动布局、定位布局、Flexbox 布局、Grid 布局、表格布局和多列布局等。现代开发中,Flexbox 和 Grid 布局被广泛使用,因为它们提供了更强大且灵活的布局能力。”
JS中的理解和this指向
箭头函数和普通函数的区别
或者
1 箭头函数比普通函数更加简洁
2 箭头函数没有自己的this
3 箭头函数继承来的this指向永远不会改变
4 call()、apply()、bind()等方法不能改变箭头函数中this的指向
5 箭头函数不能作为构造函数使用
6 箭头函数没有自己的arguments
7 箭头函数没有prototype
apply call bind得区别
apply、call、bind都可以为函数指定this
apply 和 call 就是传参方式不一样,apply 参数以一个数组的形式传入。但是两个都是会在调用的时候同时执行调用的函数。bind则会返回一个绑定了this的函数。
cookie sessionstorage localstorage
1. Cookie
Cookie 是一种在浏览器与服务器之间传递小块数据的机制。它主要用于保存用户信息、会话信息等,并且这些信息会随着每个请求一起发送到服务器。
特点
- 存储大小:每个 Cookie 大小限制为 4KB。
- 生命周期:可以设置 过期时间,如果没有设置,默认是会话结束时(浏览器关闭)失效。
- 作用域:Cookie 会随着请求一起发送到与当前 Cookie 域名匹配的服务器。即 Cookie 是基于域名的,无法跨域访问。
- 安全性:可以通过
Secure
和HttpOnly
属性提高安全性,防止 JavaScript 访问(HttpOnly
),并确保只有在 HTTPS 协议下传输(Secure
)。 - 用途:用于身份验证、用户追踪、会话管理。
1// 设置 cookie
2document.cookie = "username=JohnDoe; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
3
4// 获取 cookie
5console.log(document.cookie);
2. sessionStorage
sessionStorage 用于在浏览器会话期间存储数据,且数据仅在当前浏览器窗口或标签页中有效。
特点
- 存储大小:大约 5MB。
- 生命周期:数据在浏览器标签页或窗口关闭时 自动删除,也就是 会话结束后失效。
- 作用域:数据仅在同一个窗口或标签页中可访问,不能跨窗口、标签页或浏览器进程共享。
- 安全性:数据仅存在客户端,不能通过网络访问,安全性较高。
- 用途:用于存储临时数据,例如单页应用的会话状态。
1// 设置 sessionStorage
2sessionStorage.setItem('username', 'JohnDoe');
3
4// 获取 sessionStorage
5console.log(sessionStorage.getItem('username'));
6
7// 删除 sessionStorage
8sessionStorage.removeItem('username');
3. localStorage
localStorage 是一种在浏览器中长期存储数据的机制,数据会一直保存在浏览器中,直到手动删除。
特点
- 存储大小:通常 5MB 或更大。
- 生命周期:数据永远存在,直到手动删除,不受浏览器关闭的影响。可以在多个窗口和标签页之间共享。
- 作用域:在同一域名下的所有标签页和窗口中都可访问,数据永久保存在客户端,直到被删除。
- 安全性:与 sessionStorage 一样,数据仅存在客户端,无法通过网络访问。
- 用途:适合存储长期不变的数据,如用户设置、主题、偏好设置等。
1// 设置 localStorage
2localStorage.setItem('username', 'JohnDoe');
3
4// 获取 localStorage
5console.log(localStorage.getItem('username'));
6
7// 删除 localStorage
8localStorage.removeItem('username');
4. 区别总结
特性 | Cookie | sessionStorage | localStorage |
---|---|---|---|
存储大小 | 最多 4KB | 约 5MB | 约 5MB |
生命周期 | 可设置过期时间 | 当前会话,浏览器窗口或标签页关闭后失效 | 永久存储,直到手动删除 |
作用域 | 基于域名,跨请求发送 | 当前窗口或标签页 | 在同一域名下的所有窗口/标签页共享 |
数据传输 | 每次 HTTP 请求都自动发送到服务器 | 不随请求发送,只在浏览器中访问 | 不随请求发送,只在浏览器中访问 |
用途 | 用户身份、认证、追踪 | 临时会话数据 | 持久化存储,如设置、偏好配置等 |
总结
“Cookie 适合存储需要在服务器和浏览器之间传递的数据,sessionStorage 用于临时存储会话数据,localStorage 用于存储持久化的数据。根据不同的存储需求选择合适的方式。”
通过这种简明扼要的解释,面试官能够清楚地理解你对这三者的区别和用途的掌握。
vue传值方法
有没有了解浏览器安全,有哪些攻击和怎么防范?
我们常见的Web攻击方式有
- XSS (Cross Site Scripting) 跨站脚本攻击
- CSRF(Cross-site request forgery)跨站请求伪造
- SQL注入攻击
1. XSS(Cross-Site Scripting,跨站脚本攻击)
原理:
攻击者在网站的输入框、URL、HTTP 请求等地方注入恶意脚本代码(通常是 JavaScript),当其他用户访问该页面时,恶意脚本被执行,从而窃取用户信息或执行其他恶意操作。
防范措施:
- 输入过滤:对用户输入进行严格的字符过滤或转义(如
<font style="color:rgb(44, 62, 80);">&</font>
转换成<font style="color:rgb(44, 62, 80);">&</font>
)。 - 内容安全策略(CSP):使用
<font style="color:rgb(44, 62, 80);">Content-Security-Policy</font>
限制脚本来源,防止恶意脚本加载。 - HttpOnly Cookie:防止 JavaScript 读取
<font style="color:rgb(44, 62, 80);">document.cookie</font>
,减少 Cookie 被盗风险。 - 避免 innerHTML 动态插入 HTML:改用
<font style="color:rgb(44, 62, 80);">textContent</font>
或<font style="color:rgb(44, 62, 80);">innerText</font>
。
2. CSRF(Cross-Site Request Forgery,跨站请求伪造)
原理:
攻击者诱导用户在已登录的状态下访问恶意网站,该网站会向目标网站发起请求,由于用户已登录,浏览器会自动带上 Cookie,使请求被误认为是用户的正常操作。
防范措施:
- CSRF Token:在每次请求时携带一个唯一 Token,并在服务器端验证。
- SameSite Cookie:设置
<font style="color:rgb(44, 62, 80);">SameSite=Strict</font>
或<font style="color:rgb(44, 62, 80);">Lax</font>
,防止跨站请求自动携带 Cookie。 - Referer 头检查:检查请求来源是否符合预期。
- 验证码(CAPTCHA):阻止自动提交恶意请求。
3. SQL 注入
原理:
攻击者在输入字段(如用户名、密码输入框)中插入 SQL 代码,导致数据库执行未预期的 SQL 语句,进而获取、篡改或删除数据库中的数据。
防范措施:
- 使用预编译语句(Prepared Statements):避免 SQL 语句拼接。
- 使用 ORM(对象关系映射):如 Sequelize、Hibernate 等,减少直接操作 SQL 的风险。
- 输入校验:对用户输入进行严格限制,如仅允许字母和数字。
- 最小权限原则:限制数据库用户的权限,避免过高权限导致数据泄露或破坏。
4. Clickjacking(点击劫持)
原理:
攻击者在自己的网站上嵌套目标网站的 iframe,并通过 CSS 透明遮罩层诱导用户点击,从而执行恶意操作(如点赞、支付等)。
防范措施:
- X-Frame-Options 头:设置
<font style="color:rgb(44, 62, 80);">DENY</font>
或<font style="color:rgb(44, 62, 80);">SAMEORIGIN</font>
,防止网页被嵌套。 - Content Security Policy(CSP):
<font style="color:rgb(44, 62, 80);">frame-ancestors</font>
限制页面被嵌套的来源。 - JS 防御:在页面加载时检查
<font style="color:rgb(44, 62, 80);">window.top</font>
是否为自身,如果不是则跳出 iframe。
5. MITM(Man-in-the-Middle,中间人攻击)
原理:
攻击者拦截客户端与服务器的通信,窃取或篡改传输中的数据(如登录凭证、敏感信息等)。
防范措施:
- HTTPS(TLS):强制使用 HTTPS 传输数据,防止明文传输被窃取。
- HSTS(HTTP Strict Transport Security):强制浏览器只使用 HTTPS 访问网站。
- 避免使用不安全的 Wi-Fi:防止流量被截获。
- 公钥固定(Certificate Pinning):防止中间人伪造证书。
6. 恶意 API 调用(API Abuse)
原理:
攻击者利用 API 进行暴力破解、爬取数据、恶意操作等。
防范措施:
- API 认证(Token + 签名):使用 JWT、OAuth 等对 API 访问进行认证。
- 限流(Rate Limiting):限制单位时间内的请求次数,防止暴力攻击。
- CORS(跨域资源共享)限制:控制哪些域可以访问 API。
重绘和回流
优化回流与重绘
1:用transform 代替 top,left ,margin-top, margin-left… 这些位移属性
2:用opacity 代替 visibility,但是要同时有translate3d 或 translateZ 这些可以创建的图层的属性存在才可以阻止回流
但是第二点经过我的实验,发现如果不加 transform: translateZ(0) 配合opacity的话还是会产生回流的,而只用visibility 就只会产生重绘不会回流
而 opacity 加上 transform: translateZ/3d 这个属性之后便不会发生回流和重绘了
3:不要使用 js 代码对dom 元素设置多条样式,选择用一个 className 代替之。
4:如果确实需要用 js 对 dom 设置多条样式那么可以将这个dom 先隐藏,然后再对其设置
5:不要在循环内获取dom 的样式例如:offsetWidth, offsetHeight, clientWidth, clientHeight… 这些。浏览器有一个回流的缓冲机制,即多个回流会保存在一个栈里面,当这个栈满了浏览器便会一次性触发所有样式的更改且刷新这个栈。但是如果你多次获取这些元素的实际样式,浏览器为了给你一个准确的答案便会不停刷新这个缓冲栈,导致页面回流增加。
所以为了避免这个问题,应该用一个变量保存在循环体外。
6:不要使用table 布局,因为table 的每一个行甚至每一个单元格的样式更新都会导致整个table 重新布局
7:动画的速度按照业务按需决定
8:对于频繁变化的元素应该为其加一个 transform 属性,对于视频使用video 标签
9:必要时可以开启 GPU 加速,但是不能滥用
VUE2和VUE3的区别
1. 生命周期
对于生命周期来说,整体上变化不大,只是大部分生命周期钩子名称上 + “on”,功能上是类似的。不过有一点需要注意,Vue3 在组合式API(Composition API,下面展开)中使用生命周期钩子时需要先引入,而 Vue2 在选项API(Options API)中可以直接调用生命周期钩子,如下所示。
1// vue3
2<script setup>
3import { onMounted } from 'vue'; // 使用前需引入生命周期钩子
4
5onMounted(() => {
6 // ...
7});
8
9// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
10onMounted(() => {
11 // ...
12});
13</script>
14
15// vue2
16<script>
17export default { mounted() { // 直接调用生命周期钩子
18 // ...
19 }, }
20</script>
常用生命周期对比如下表所示。
vue2 | vue3 |
---|---|
beforeCreate | |
created | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
Tips: setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义。
2. 多根节点
熟悉 Vue2 的朋友应该清楚,在模板中如果使用多个根节点时会报错,如下所示。
1// vue2中在template里存在多个根节点会报错
2<template>
3 <header></header>
4 <main></main>
5 <footer></footer>
6</template>
7
8// 只能存在一个根节点,需要用一个<div>来包裹着
9<template>
10 <div>
11 <header></header>
12 <main></main>
13 <footer></footer>
14 </div>
15</template>
但是,Vue3 支持多个根节点,也就是 fragment。即以下多根节点的写法是被允许的。
1<template>
2 <header></header>
3 <main></main>
4 <footer></footer>
5</template>
3. Composition API
Vue2 是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。
4. 异步组件(Suspense)
Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。参考 前端进阶面试题详细解答
1<tempalte>
2 <suspense>
3 <template #default>
4 <List />
5 </template>
6 <template #fallback>
7 <div>
8 Loading... </div>
9 </template>
10 </suspense>
11</template>
在 List 组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示 Loading…(即 fallback 插槽内容),加载完成时显示自身(即 default 插槽内容)。
5. Teleport
Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗。
1<button @click="dialogVisible = true">显示弹窗</button>
2<teleport to="body">
3 <div class="dialog" v-if="dialogVisible">
4 我是弹窗,我直接移动到了body标签下 </div>
5</teleport>
6. 响应式原理
Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。
- Object.defineProperty 基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。
1let obj = {};
2let name = 'leo';
3Object.defineProperty(obj, 'name', {
4 enumerable: true, // 可枚举(是否可通过 for...in 或 Object.keys() 进行访问)
5 configurable: true, // 可配置(是否可使用 delete 删除,是否可再次设置属性)
6 // value: '', // 任意类型的值,默认undefined
7 // writable: true, // 可重写
8 get() {
9 return name;
10 },
11 set(value) {
12 name = value;
13 }
14});
Tips: writable
和 value
与 getter
和 setter
不共存。
搬运 Vue2 核心源码,略删减。
1function defineReactive(obj, key, val) {
2 // 一 key 一个 dep
3 const dep = new Dep()
4
5 // 获取 key 的属性描述符,发现它是不可配置对象的话直接 return
6 const property = Object.getOwnPropertyDescriptor(obj, key)
7 if (property && property.configurable === false) { return }
8
9 // 获取 getter 和 setter,并获取 val 值
10 const getter = property && property.get
11 const setter = property && property.set
12 if((!getter || setter) && arguments.length === 2) { val = obj[key] }
13
14 // 递归处理,保证对象中所有 key 被观察
15 let childOb = observe(val)
16
17 Object.defineProperty(obj, key, {
18 enumerable: true,
19 configurable: true,
20 // get 劫持 obj[key] 的 进行依赖收集
21 get: function reactiveGetter() {
22 const value = getter ? getter.call(obj) : val
23 if(Dep.target) {
24 // 依赖收集
25 dep.depend()
26 if(childOb) {
27 // 针对嵌套对象,依赖收集
28 childOb.dep.depend()
29 // 触发数组响应式
30 if(Array.isArray(value)) {
31 dependArray(value)
32 }
33 }
34 }
35 }
36 return value
37 })
38 // set 派发更新 obj[key]
39 set: function reactiveSetter(newVal) {
40 ...
41 if(setter) {
42 setter.call(obj, newVal)
43 } else {
44 val = newVal
45 }
46 // 新值设置响应式
47 childOb = observe(val)
48 // 依赖通知更新
49 dep.notify()
50 }
51}
那 Vue3 为何会抛弃它呢?那肯定是因为它存在某些局限性。
主要原因:无法监听对象或数组新增、删除的元素。
Vue2 相应解决方案:针对常用数组原型方法push、pop、shift、unshift、splice、sort、reverse进行了hack处理;提供Vue.set监听对象/数组新增属性。对象的新增/删除响应,还可以new个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
- Proxy Proxy 是 ES6 新特性,通过第2个参数 handler 拦截目标对象的行为。相较于 Object.defineProperty 提供语言全范围的响应能力,消除了局限性。
局限性:
(1)、对象/数组的新增、删除
(2)、监测 .length 修改
(3)、Map、Set、WeakMap、WeakSet 的支持
基本用法:创建对象的代理,从而实现基本操作的拦截和自定义操作。
1let handler = {
2 get(obj, prop) {
3 return prop in obj ? obj[prop] : '';
4 },
5 set() {
6 // ...
7 },
8 ...
9};
搬运 vue3 的源码 reactive.ts 文件。
1function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap) {
2 ...
3 // collectionHandlers: 处理Map、Set、WeakMap、WeakSet
4 // baseHandlers: 处理数组、对象
5 const proxy = new Proxy(
6 target,
7 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
8 )
9 proxyMap.set(target, proxy)
10 return proxy
11}
7. 虚拟DOM
Vue3 相比于 Vue2,虚拟DOM上增加 patchFlag 字段。我们借助Vue3 Template Explorer来看。
1<div id="app">
2 <h1>vue3虚拟DOM讲解</h1>
3 <p>今天天气真不错</p>
4 <div>{{name}}</div>
5</div>
渲染函数如下所示。
1import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue
2
3const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
4const _hoisted_1 = { id: app }
5const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3虚拟DOM讲解, -1 /* HOISTED */))
6const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
7
8export function render(_ctx, _cache, $props, $setup, $data, $options) {
9 return (_openBlock(), _createElementBlock(div, _hoisted_1, [
10 _hoisted_2,
11 _hoisted_3,
12 _createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */)
13 ]))
14}
注意第3个_createElementVNode的第4个参数即 patchFlag 字段类型。
字段类型情况:1 代表节点为动态文本节点,那在 diff 过程中,只需比对文本对容,无需关注 class、style等。除此之外,发现所有的静态节点(HOISTED 为 -1),都保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建。
1// patchFlags 字段类型列举
2export const enum PatchFlags {
3 TEXT = 1, // 动态文本内容
4 CLASS = 1 << 1, // 动态类名
5 STYLE = 1 << 2, // 动态样式
6 PROPS = 1 << 3, // 动态属性,不包含类名和样式
7 FULL_PROPS = 1 << 4, // 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
8 HYDRATE_EVENTS = 1 << 5, // 带有监听事件的节点
9 STABLE_FRAGMENT = 1 << 6, // 不会改变子节点顺序的 fragment
10 KEYED_FRAGMENT = 1 << 7, // 带有 key 属性的 fragment 或部分子节点
11 UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的fragment
12 NEED_PATCH = 1 << 9, // 只会进行非 props 的比较
13 DYNAMIC_SLOTS = 1 << 10, // 动态的插槽
14 HOISTED = -1, // 静态节点,diff阶段忽略其子节点
15 BAIL = -2 // 代表 diff 应该结束
16}
8. 事件缓存
Vue3 的cacheHandler
可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数。加一个 click 事件。
1<div id="app">
2 <h1>vue3事件缓存讲解</h1>
3 <p>今天天气真不错</p>
4 <div>{{name}}</div>
5 <span onCLick=() => {}><span>
6</div>
渲染函数如下所示。
1import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue
2
3const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
4const _hoisted_1 = { id: app }
5const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3事件缓存讲解, -1 /* HOISTED */))
6const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
7const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [
8 /*#__PURE__*/_createElementVNode(span)
9], -1 /* HOISTED */))
10
11export function render(_ctx, _cache, $props, $setup, $data, $options) {
12 return (_openBlock(), _createElementBlock(div, _hoisted_1, [
13 _hoisted_2,
14 _hoisted_3,
15 _createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),
16 _hoisted_4
17 ]))
18}
观察以上渲染函数,你会发现 click 事件节点为静态节点(HOISTED 为 -1),即不需要每次重新渲染。
9. Diff算法优化
搬运 Vue3 patchChildren 源码。结合上文与源码,patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。
1function patchChildren(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
2 // 获取新老孩子节点
3 const c1 = n1 && n1.children
4 const c2 = n2.children
5 const prevShapeFlag = n1 ? n1.shapeFlag : 0
6 const { patchFlag, shapeFlag } = n2
7
8 // 处理 patchFlag 大于 0
9 if(patchFlag > 0) {
10 if(patchFlag && PatchFlags.KEYED_FRAGMENT) {
11 // 存在 key
12 patchKeyedChildren()
13 return
14 } els if(patchFlag && PatchFlags.UNKEYED_FRAGMENT) {
15 // 不存在 key
16 patchUnkeyedChildren()
17 return
18 }
19 }
20
21 // 匹配是文本节点(静态):移除老节点,设置文本节点
22 if(shapeFlag && ShapeFlags.TEXT_CHILDREN) {
23 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
24 unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
25 }
26 if (c2 !== c1) {
27 hostSetElementText(container, c2 as string)
28 }
29 } else {
30 // 匹配新老 Vnode 是数组,则全量比较;否则移除当前所有的节点
31 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
32 if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
33 patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense,...)
34 } else {
35 unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
36 }
37 } else {
38
39 if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
40 hostSetElementText(container, '')
41 }
42 if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
43 mountChildren(c2 as VNodeArrayChildren, container,anchor,parentComponent,...)
44 }
45 }
46 }
47}
patchUnkeyedChildren 源码如下所示。
1function patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
2 c1 = c1 || EMPTY_ARR
3 c2 = c2 || EMPTY_ARR
4 const oldLength = c1.length
5 const newLength = c2.length
6 const commonLength = Math.min(oldLength, newLength)
7 let i
8 for(i = 0; i < commonLength; i++) {
9 // 如果新 Vnode 已经挂载,则直接 clone 一份,否则新建一个节点
10 const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as Vnode)) : normalizeVnode(c2[i])
11 patch()
12 }
13 if(oldLength > newLength) {
14 // 移除多余的节点
15 unmountedChildren()
16 } else {
17 // 创建新的节点
18 mountChildren()
19 }
20
21}
10. 打包优化
Tree-shaking:模块打包 webpack、rollup 等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
以 nextTick 为例子,在 Vue2 中,全局API暴露在Vue实例上,即使未使用,也无法通过 tree-shaking 进行消除。
1import Vue from 'vue';
2
3Vue.nextTick(() => {
4 // 一些和DOM有关的东西
5});
Vue3 中针对全局和内部的API进行了重构,并考虑到 tree-shaking 的支持。因此,全局API现在只能作为ES模块构建的命名导出进行访问。
1import { nextTick } from 'vue'; // 显式导入
2
3nextTick(() => {
4 // 一些和DOM有关的东西
5});
通过这一更改,只要模块绑定器支持 tree-shaking,则Vue应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小。
受此更改影响的全局API如下所示。
- Vue.nextTick
- Vue.observable (用 Vue.reactive 替换)
- Vue.version
- Vue.compile (仅全构建)
- Vue.set (仅兼容构建)
- Vue.delete (仅兼容构建)
内部API也有诸如 transition、v-model 等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。Vue3 将所有运行功能打包也只有约22.5kb,比 Vue2 轻量很多。
11. TypeScript支持
Vue3 由 TypeScript 重写,相对于 Vue2 有更好的 TypeScript 支持。
- Vue2 Options API 中 option 是个简单对象,而 TypeScript 是一种类型系统,面向对象的语法,不是特别匹配。
- Vue2 需要vue-class-component强化vue原生组件,也需要vue-property-decorator增加更多结合Vue特性的装饰器,写法比较繁琐。
如何解决首屏白屏,加载速度慢
加载慢的原因
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
解决方案
- 减小入口文件积
- 静态资源本地缓存
- UI框架按需加载
- 图片资源的压缩
- 组件重复打包
- 开启GZip压缩
- 使用SSR
减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在<font style="color:rgb(71, 101, 130);">vue-router</font>
配置路由的时候,采用动态加载路由的形式
1routes:[
2 path: 'Blogs',
3 name: 'ShowBlogs',
4 component: () => import('./components/ShowBlogs.vue')
5]
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
#静态资源本地缓存
后端返回资源问题:
- 采用
<font style="color:rgb(71, 101, 130);">HTTP</font>
缓存,设置<font style="color:rgb(71, 101, 130);">Cache-Control</font>
,<font style="color:rgb(71, 101, 130);">Last-Modified</font>
,<font style="color:rgb(71, 101, 130);">Etag</font>
等响应头 - 采用
<font style="color:rgb(71, 101, 130);">Service Worker</font>
离线缓存
前端合理利用<font style="color:rgb(71, 101, 130);">localStorage</font>
#UI框架按需加载
在日常使用<font style="color:rgb(71, 101, 130);">UI</font>
框架,例如<font style="color:rgb(71, 101, 130);">element-UI</font>
、或者<font style="color:rgb(71, 101, 130);">antd</font>
,我们经常性直接引用整个<font style="color:rgb(71, 101, 130);">UI</font>
库
1import ElementUI from 'element-ui'
2Vue.use(ElementUI)
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
1import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
2Vue.use(Button)
3Vue.use(Input)
4Vue.use(Pagination)
#组件重复打包
假设<font style="color:rgb(71, 101, 130);">A.js</font>
文件是一个常用的库,现在有多个路由使用了<font style="color:rgb(71, 101, 130);">A.js</font>
文件,这就造成了重复下载
解决方案:在<font style="color:rgb(71, 101, 130);">webpack</font>
的<font style="color:rgb(71, 101, 130);">config</font>
文件中,修改<font style="color:rgb(71, 101, 130);">CommonsChunkPlugin</font>
的配置
1minChunks: 3
<font style="color:rgb(71, 101, 130);">minChunks</font>
为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
#图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的<font style="color:rgb(71, 101, 130);">icon</font>
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻<font style="color:rgb(71, 101, 130);">http</font>
请求压力。
#开启GZip压缩
拆完包之后,我们再用<font style="color:rgb(71, 101, 130);">gzip</font>
做一下压缩 安装<font style="color:rgb(71, 101, 130);">compression-webpack-plugin</font>
1cnmp i compression-webpack-plugin -D
在<font style="color:rgb(71, 101, 130);">vue.congig.js</font>
中引入并修改<font style="color:rgb(71, 101, 130);">webpack</font>
配置
1const CompressionPlugin = require('compression-webpack-plugin')
2
3configureWebpack: (config) => {
4 if (process.env.NODE_ENV === 'production') {
5 // 为生产环境修改配置...
6 config.mode = 'production'
7 return {
8 plugins: [new CompressionPlugin({
9 test: /\.js$|\.html$|\.css/, //匹配文件名
10 threshold: 10240, //对超过10k的数据进行压缩
11 deleteOriginalAssets: false //是否删除原文件
12 })]
13 }
14 }
在服务器我们也要做相应的配置 如果发送请求的浏览器支持<font style="color:rgb(71, 101, 130);">gzip</font>
,就发送给它<font style="color:rgb(71, 101, 130);">gzip</font>
格式的文件 我的服务器是用<font style="color:rgb(71, 101, 130);">express</font>
框架搭建的 只要安装一下<font style="color:rgb(71, 101, 130);">compression</font>
就能使用
1const compression = require('compression')
2app.use(compression()) // 在其他中间件使用之前调用
#使用SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,<font style="color:rgb(71, 101, 130);">vue</font>
应用建议使用<font style="color:rgb(71, 101, 130);">Nuxt.js</font>
实现服务端渲染
hash 模式和 history 模式有什么区别
区别:
1.url 展示上,hash 模式有“#”,history 模式没有
2.刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由。
3.兼容性。hash 可以支持低版本浏览器和 IE。
Promise 和async await的区别
1. 本质区别
“Promise 和 async/await 都是用来处理异步操作的,Promise 是基于回调函数**,而 async/await 是基于 Promise 的语法糖,让异步代码看起来像同步代码,更清晰易读。**”
2. Promise 和 async/await 的核心区别
对比点 | Promise | async/await |
---|---|---|
语法风格 | .then().catch() 链式调用 | await 让代码像同步执行 |
可读性 | 代码较嵌套,容易形成回调地狱 | 更直观、清晰,像同步代码 |
错误处理 | .catch() 处理异常 | try...catch 处理异常,更优雅 |
返回值 | Promise 对象 | await 返回值是Promise 解析后的值 |
并发执行 | 需要手动 Promise.all() | 需要手动 Promise.all() ,否则是串行执行 |
调试难度 | then 链式调用,容易混乱 | async/await 更容易调试和阅读 |
3并发执行(Promise.all)
如果多个异步任务可以并行执行,用 Promise.all()
提高性能。
错误用法(async/await 串行执行,慢)
1async function fetchData() {
2 const res1 = await fetch("https://api.example.com/data1");
3 const res2 = await fetch("https://api.example.com/data2");
4 console.log(res1, res2);
5}
⚠️ 这里 res1 和 res2 是依次执行,导致性能浪费。
正确用法(Promise.all 并行执行,快)
1async function fetchData() {
2 const [res1, res2] = await Promise.all([
3 fetch("https://api.example.com/data1"),
4 fetch("https://api.example.com/data2")
5 ]);
6 console.log(res1, res2);
7}
✅** 这样两个请求同时执行,速度更快!**
4. 什么时候用 Promise?什么时候用 async/await?
适用场景 | 选择 |
---|---|
需要并发执行多个异步任务 | Promise.all() |
代码逻辑简单,链式调用较少 | Promise.then().catch() |
代码可读性优先,避免回调地狱 | async/await |
需要顺序执行多个异步任务 | async/await |
5. 总结
“Promise 和 async/await 都能处理异步,async/await 让代码更清晰、易读,但并发执行时需要 Promise.all()
。对于简单的链式操作,可以用 Promise,但如果代码逻辑复杂,async/await 更直观。“**
vue单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
单项数据流,父組件通过props向子组件传递值,子组件通过emit事件来通知父组件修改值,子组件不在自身对父组件传递过来的props做任何修改,都是通过父组件来更新props,从而达到子组件更新自身状态。
原生js如何写一个轮播图
可以的,我会使用 JavaScript 操作 DOM 元素,结合 setInterval
实现自动播放,并提供手动切换功能。具体实现思路如下:
1. 结构设计
- 我会创建一个
div
作为轮播容器,内部包含多个img
作为轮播图片。 - 还会添加“上一张”和“下一张”按钮,以及小圆点指示器(可选)。
2. 样式布局
- 轮播区域使用
overflow: hidden;
隐藏溢出的图片。 - 图片排列使用
display: flex;
,并通过transform: translateX()
控制滚动。 - 切换时添加
transition: 0.5s
,实现平滑过渡。
3. 主要功能实现
(1)图片切换逻辑
- 维护一个
index
变量,表示当前显示的图片索引。 - 点击“下一张”按钮时,让
index + 1
,修改translateX
实现滑动。 - 点击“上一张”按钮时,让
index - 1
,实现回退。 - 如果
index
超过图片数量,重置到第一张,实现循环播放。
(2)自动播放
- 使用
setInterval
,每隔 3 秒调用“下一张”函数,自动轮播。 - 鼠标移入时暂停(
clearInterval
),鼠标移出时恢复(setInterval
)。
(3)小圆点指示器(如果有)
- 根据
index
高亮对应的小圆点,并监听点击,实现跳转到指定图片。
4. 代码示例
(此处你可以简要概括核心逻辑,不需要完整代码)
1let index = 0;
2const images = document.querySelector('.images');
3
4function nextSlide() {
5 index = (index + 1) % images.children.length;
6 images.style.transform = `translateX(${-index * 600}px)`;
7}
8
9setInterval(nextSlide, 3000); // 自动轮播
5. 可能的优化点
- 适配移动端触摸滑动(监听
touchstart
和touchmove
事件)。 - 采用
requestAnimationFrame
代替setInterval
,优化动画流畅度。 - 添加无缝滚动(克隆第一张和最后一张图片,实现更自然的循环)。
总结
轮播图的核心是 改变 **translateX**
实现切换,结合 定时器 和 事件监听 实现自动播放和手动切换。根据需求,还可以添加指示器、优化滑动效果等。
原型和原型链(问的真多)
说说对原型的理解(面试题)
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象,使用原型对象的好处是所有对象实例共享它所包含的属性和方法
什么是原型链(面试题)
每个对象拥有一个原型对象,通过 proto 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null (Object.proptotype._proto_指向的是null)。这种关系被称为原型链,通过原型链一个对象可以拥有定义在其他对象中的属性和方法
或者
原型链
实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念。
原型
在js中,每个构造函数内部都有一个prototype属性,该属性的值是个对象,该对象包含了该构造函数所有实例共享的属性和方法。当我们通过构造函数创建对象的时候,在这个对象中有一个指针,这个指针指向构造函数的prototype的值,我们将这个指向prototype的指针称为原型。
闭包(问的真多)
闭包是一个函数有权访问另一个函数(另一个作用域)中声明的变量,这个函数就叫闭包
闭包就是每次调用外层函数时,临时创建的函数作用域对象。
因为内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,这个时候就形成了闭包。
所以闭包通常是在函数嵌套中形成的。
好处
①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
③匿名自执行函数可以减少内存消耗
坏处
①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
闭包使用场景
实现块级作用域
首先我们来看这样一段代码:
可以看到,每个函数并不像我们期待的那样 result0 打印 0,result1 打印 1,以此类推。
因为 var 声明的 i 不只是属于当前的每一次循环,甚至不只是属于当前的 for 循环,因为没有块级作用域,变量 i 被提升到了函数 foo 的作用域中。所以每个函数的作用域链中都保存着同一个变量 i,而当我们执行数组中的子函数时,此时 foo 内部的循环已经结束,此时 i = 10,所以每个函数调用都会打印 10。
接下来我们对 for 循环内部添加一层即时函数(又叫立即执行函数 IIFE),形成一个新的闭包环境,这样即时函数内部就保存了本次循环的 i,所以再次执行数组中子函数时,结果就像我们期望的那样 result0 打印 0,result1 打印 1 …
当然,ES6 中引入 let 声明变量方式,让 JavaScript 拥有了块级作用域,可以更方便的解决这样的一个问题。
保存内部状态
首先我们来看这样一段代码:
可以看到,函数内部会使用 Map 保存已经计算过的结果(当然也可以是其他的数据结构),只有当输入数字没有被计算过时,才会计算,否则会返回之前的计算结果,这样就会避免重复计算。
而这样的技巧在 Vue3源码 中同样有使用到。代码地址
这里我在阅读源码的过程中加了一些注释,导致截图中代码行号和源文件中的不一致,但是代码并未进行任何修改。
闭包一般在项目哪里用
1. 数据私有化(模拟私有变量)
在 JavaScript 中,没有原生的 private
关键字,但闭包可以用来创建私有变量,避免外部访问和修改。
应用场景:封装组件/工具类
在 Vue/React 组件或工具函数中,闭包可以用于存储内部状态,防止外部修改。
示例:计数器
1function createCounter() {
2 let count = 0; // 私有变量,外部无法直接访问
3
4 return {
5 increment() {
6 count++;
7 console.log(count);
8 },
9 decrement() {
10 count--;
11 console.log(count);
12 },
13 getCount() {
14 return count;
15 }
16 };
17}
18
19const counter = createCounter();
20counter.increment(); // 1
21counter.increment(); // 2
22console.log(counter.getCount()); // 2
23console.log(counter.count); // undefined(外部无法访问)
项目中应用:
- 用于封装工具函数(如防抖、节流)
- 用于 Vue 组件中的组合式 API
- Vuex/Pinia store 的 getter 计算属性
2. 防抖和节流
防抖和节流是优化事件触发频率的重要方法,通常用于 scroll
、resize
、input
等事件,避免频繁调用函数。
应用场景:搜索框输入防抖
1function debounce(fn, delay) {
2 let timer;
3 return function(...args) {
4 if (timer) clearTimeout(timer);
5 timer = setTimeout(() => {
6 fn.apply(this, args);
7 }, delay);
8 };
9}
10
11// 用法
12const searchInput = document.getElementById('search');
13searchInput.addEventListener('input', debounce((e) => {
14 console.log('搜索关键词:', e.target.value);
15}, 300));
项目中应用:
- 搜索框防抖
- 窗口resize 事件优化
- 按钮点击节流,防止多次点击
3. 柯里化(函数预处理)
闭包可以用于函数柯里化,提前存储一部分参数,提高代码复用性。
应用场景:通用日志打印
1function logger(level) {
2 return function(message) {
3 console.log(`[${level}]: ${message}`);
4 };
5}
6
7const logInfo = logger("INFO");
8const logError = logger("ERROR");
9
10logInfo("This is an info message."); // [INFO]: This is an info message.
11logError("This is an error message."); // [ERROR]: This is an error message.
项目中应用:
- 日志封装(debug、error、info)
- 动态权限控制
- 格式化工具(数字、日期等)
4. 高阶函数(函数式编程)
闭包在高阶函数中常用于封装通用逻辑,提高代码复用性。
应用场景:权限校验
1function checkPermission(role) {
2 return function(action) {
3 if (role === "admin") {
4 console.log(`允许执行 ${action}`);
5 } else {
6 console.log(`禁止执行 ${action}`);
7 }
8 };
9}
10
11const adminActions = checkPermission("admin");
12const userActions = checkPermission("user");
13
14adminActions("删除用户"); // 允许执行 删除用户
15userActions("删除用户"); // 禁止执行 删除用户
项目中应用:
- 权限控制
- 请求封装
- 拦截器(如
Axios
请求封装)
5. 记忆化(缓存计算结果,提高性能)
闭包可以用于缓存计算结果,减少重复计算,提高程序性能。
应用场景:计算结果缓存
1function memoize(fn) {
2 let cache = {};
3 return function(...args) {
4 const key = JSON.stringify(args);
5 if (cache[key]) {
6 console.log("返回缓存结果");
7 return cache[key];
8 }
9 const result = fn(...args);
10 cache[key] = result;
11 return result;
12 };
13}
14
15// 用法
16const expensiveCalculation = memoize((num) => {
17 console.log("计算中...");
18 return num * num;
19});
20
21console.log(expensiveCalculation(5)); // 计算中... 25
22console.log(expensiveCalculation(5)); // 返回缓存结果 25
项目中应用:
- 减少 API 请求(缓存接口数据)
- Vue 计算属性缓存
- 前端数据存储优化
总结
应用场景 | 具体使用方式 |
---|---|
数据私有化 | 封装组件状态、避免全局变量污染 |
防抖 & 节流 | 限制事件触发频率,如输入框搜索、滚动事件 |
柯里化 | 预设参数,提高代码复用性 |
高阶函数 | 用于权限控制、拦截器等 |
记忆化 | 提高性能,减少计算或 API 请求 |
面试问答:
问:闭包一般在哪些场景下使用?
答:闭包主要用于数据私有化、防抖和节流、函数柯里化、权限控制、高阶函数和缓存计算结果等。在实际项目中,闭包常用于封装工具函数、优化性能、封装 Vue 组件状态,以及拦截器和权限控制等场景。
垃圾回收机制
浏览器常用的垃圾回收方法有两种:标记清除、引用计数
(1) 标记清除:
- 标记清除是浏览器最常见的垃圾回收方式,其原理是当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量时不能被回收的,因为它们正在被使用。当变量离开环境的时候,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后会对环境中的变量以及被环境中的变量引用的标记去掉。此后再被加上标记的变量将会被看做准备要删除的变量,最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并返回它们所占用的内存空间
(2)引用计数:
-
另一种的垃圾回收机制就是引用计数,这个用的相对较少。其原理是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。当包含这个的值引用的变量又取得了另一个值,则这个值的引用次数就减1。直到这个引用次数变为0的时候,说明这个变量已经没有价值了,在垃圾回收下次运行的时候,这个变量所占用的内存空间就会被释放。
-
这种方法的缺点是会引起循环引用的问题:例如:
(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
- 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
(4)哪些情况会导致内存泄漏
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
typeof和intanceof区别
后端返回十万条数据前端怎么处理
“如果后端一次性返回 10 万条数据,直接渲染会导致 页面卡顿、性能下降,甚至浏览器崩溃。
解决方案主要有 4 种:
1. 分页加载(后端支持)
- 让后端支持分页 API,前端每次请求一小部分数据,比如 每次 1000 条,等用户 滚动到底部再加载。这样可以减少一次性渲染的数据量,提升性能。
2. 虚拟滚动(Virtual Scrolling)
- 如果数据是列表形式,可以用 虚拟滚动技术,让页面 只渲染可见部分的数据,其余数据不渲染,当用户滚动时动态加载,这样能大幅降低 DOM 负担。
3. Web Worker(多线程优化)
- 如果数据需要复杂计算或者格式转换,可以使用 Web Worker 把数据处理放到子线程,避免主线程卡死。这样 UI 不会受到影响,用户体验更流畅。
4. 按需加载(懒加载)
- 只有用户需要时才加载数据,比如 点击详情时才请求数据,这样可以减少初始加载的压力。
综合来看,最佳方案是:后端支持分页,前端结合虚拟滚动,如果涉及复杂计算,再用 Web Worker 进行优化。“
如果有个大文件要前端存储,怎么存储
“如果前端要存储一个大文件,我们首先要考虑文件的大小和存储方式。浏览器的存储空间有限,选择合适的方案很重要。
1. 小文件(几 MB 以内) → LocalStorage / IndexedDB
- LocalStorage 适合 5MB 以内的小文件,但它是同步存储,会阻塞主线程,不推荐存大文件。
- IndexedDB 是浏览器提供的异步数据库,支持存储几十 MB 到几百 MB,适合存储 JSON、二进制数据、文件等。
2. 大文件(几十 MB - 几百 MB) → IndexedDB / File System API
- IndexedDB 可以存储文件,但读取时需要解码。
- File System Access API(现代浏览器支持)可以直接存到用户本地硬盘,适用于离线存储大文件。
3. 超大文件(上 GB) → 切片存储 + 流式处理 + 离线缓存
- 切片存储:如果文件很大,可以按 1MB - 5MB 切片存到 IndexedDB,按需读取,避免一次性加载占用内存。
- 流式存储(Stream API):避免一次性加载,边下载边存储。
- 离线缓存(Service Worker + Cache API):适用于视频、图片、PWA 应用,让用户离线时也能访问数据。
4. 服务器存储(云存储 + 本地索引)
- 对于真正超大的文件(比如 10GB 以上的文件),通常不直接存储在前端,而是存储索引,文件本体放在服务器(如 OSS / S3),前端按需下载。
总结:如果是几十 MB,IndexedDB 最合适;如果是 GB 级别,建议切片存储或者放服务器,只在前端存索引。“
vue2,vue3的v-if和v-for为什么不能一起用
key的作用是啥
在 Vue 中,**key**
** 主要用于 **v-for**
列表渲染,帮助 Vue 识别哪些元素发生了变化,从而提高渲染性能,避免不必要的 DOM 操作。它的核心作用有三个:**
- 唯一性:每个
key
都应该是唯一的,Vue 通过key
确保组件或 DOM 元素能够被正确复用。 - 优化性能:当列表数据更新时,Vue 通过
key
精确比较新旧节点,避免不必要的 DOM 重新渲染。 - 防止 UI 错乱:如果
key
乱用,比如使用index
作为key
,当数据顺序发生变化时,Vue 可能会错误复用旧的 DOM,导致输入框数据错乱、动画异常等问题。
举个例子,如果我有一个列表:
1<ul>
2 <li v-for="item in list" :key="item.id">{{ item.text }}</li>
3</ul>
这里 key
使用的是 item.id
,这样即使数据发生变化,Vue 也能正确更新 UI,避免渲染错误。
如果 key
写成 index
,比如 :key="index"
,那么当我在列表头部插入新数据时,Vue 可能会复用错误的 DOM,导致输入框内容错位,用户体验变差。
所以,在 Vue 中,**key**
** 的最佳实践是使用唯一且稳定的 ID,而不是索引,以确保列表渲染的稳定性和高效性。**”
事件循环的机制是什么
回答思路:先讲 JavaScript 是单线程语言,然后解释 同步任务、异步任务(宏任务 & 微任务)、事件循环(Event Loop),最后可以用 示例代码 说明执行顺序。
1. 事件循环(Event Loop)是什么?
事件循环(Event Loop) 是 JavaScript 处理 异步任务 的机制,它决定了任务的执行顺序。
JavaScript 是 单线程 语言,所有任务都在一个主线程上执行。但如果所有代码 同步执行,遇到耗时任务(如 setTimeout
、网络请求),会导致页面卡死。
为了解决这个问题,JavaScript 采用了 事件循环机制 来处理异步任务,不会阻塞主线程。
2. 任务分类:同步任务 vs. 异步任务
(1)同步任务(Synchronous)
- 直接在 主线程 上执行的任务,如 普通函数、变量声明、循环 等。
- 执行顺序:按代码书写顺序依次执行。
(2)异步任务(Asynchronous)
- 由 回调函数 处理的任务,暂时挂起,等条件满足后再执行。
- 常见的异步任务:
setTimeout
/setInterval
- Promise /
async-await
fetch
/XMLHttpRequest
- **事件监听(click、DOMContentLoaded)`
- Node.js 相关 API(如
**fs.readFile**
)
3. 宏任务(Macrotask) vs. 微任务(Microtask)
(1)宏任务(Macrotask)
每次事件循环(Event Loop)时,先执行一个宏任务,再执行所有微任务
- 宏任务队列(Macrotask Queue):
setTimeout
setInterval
setImmediate
(Node.js)- UI 渲染
- I/O 任务(文件、网络请求等)
requestAnimationFrame
(浏览器)
(2)微任务(Microtask)
在当前宏任务执行完后立即执行,优先级比宏任务高
- 微任务队列(Microtask Queue):
Promise.then()
MutationObserver
(监听 DOM 变化)queueMicrotask
(手动添加微任务)process.nextTick()
(Node.js)
4. 事件循环(Event Loop)执行顺序
事件循环的规则
- 先执行同步任务,然后进入事件循环(Event Loop)。
- 从宏任务队列中取出一个宏任务 并执行。
- 执行所有微任务(直到微任务队列清空)。
- 检查是否有 UI 渲染(浏览器环境)。
- 重复步骤 2~4,直到所有任务执行完毕。
5. 重点总结
✅** JavaScript 是单线程,但通过 事件循环 处理异步任务。
✅ 任务分为同步任务和异步任务,异步任务分成 宏任务(Macrotask)和微任务(Microtask)。
✅ 微任务的优先级高于宏任务,会在当前宏任务执行完后 立即执行。
✅ 每次事件循环,都会 取出一个宏任务 → 执行所有微任务 → 触发 UI 渲染。**
6. 面试官可能的追问
- Promise 和 setTimeout 谁先执行?
**Promise.then()**
是微任务**,比**setTimeout()**
(宏任务)先执行。**
- 为什么 setTimeout 设为 0 还是有延迟?
- 因为
**setTimeout(0)**
依然是 宏任务,必须等待 主线程 & 所有微任务执行完 才会运行。
- 因为
- Node.js 的事件循环和浏览器有区别吗?
- Node.js 事件循环 有 6 个阶段,并且
**process.nextTick()**
的优先级比微任务还高。
- Node.js 事件循环 有 6 个阶段,并且
数组去重的方法
双循环去重
双重for(或while)循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,那么将会非常耗费内存
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 let res = [arr[0]]
7 for (let i = 1; i < arr.length; i++) {
8 let flag = true
9 for (let j = 0; j < res.length; j++) {
10 if (arr[i] === res[j]) {
11 flag = false;
12 break
13 }
14 }
15 if (flag) {
16 res.push(arr[i])
17 }
18 }
19 return res
20}
indexOf方法去重1
数组的indexOf()方法可返回某个指定的元素在数组中首次出现的位置。该方法首先定义一个空数组res,然后调用indexOf方法对原来的数组进行遍历判断,如果元素不在res中,则将其push进res中,最后将res返回即可获得去重的数组
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 let res = []
7 for (let i = 0; i < arr.length; i++) {
8 if (res.indexOf(arr[i]) === -1) {
9 res.push(arr[i])
10 }
11 }
12 return res
13}
indexOf方法去重2
利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 return Array.prototype.filter.call(arr, function(item, index){
7 return arr.indexOf(item) === index;
8 });
9}
相邻元素去重
这种方法首先调用了数组的排序方法sort(),然后根据排序后的结果进行遍历及相邻元素比对,如果相等则跳过改元素,直到遍历结束
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 arr = arr.sort()
7 let res = []
8 for (let i = 0; i < arr.length; i++) {
9 if (arr[i] !== arr[i-1]) {
10 res.push(arr[i])
11 }
12 }
13 return res
14}
利用对象属性去重
创建空对象,遍历数组,将数组中的值设为对象的属性,并给该属性赋初始值1,每出现一次,对应的属性值增加1,这样,属性值对应的就是该元素出现的次数了
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 let res = [],
7 obj = {}
8 for (let i = 0; i < arr.length; i++) {
9 if (!obj[arr[i]]) {
10 res.push(arr[i])
11 obj[arr[i]] = 1
12 } else {
13 obj[arr[i]]++
14 }
15 }
16 return res
17}
set与解构赋值去重
ES6中新增了数据类型set,set的一个最大的特点就是数据不重复。Set函数可以接受一个数组(或类数组对象)作为参数来初始化,利用该特性也能做到给数组去重
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 return [...new Set(arr)]
7}
Array.from与set去重
Array.from方法可以将Set结构转换为数组结果,而我们知道set结果是不重复的数据集,因此能够达到去重的目的
1function unique(arr) {
2 if (!Array.isArray(arr)) {
3 console.log('type error!')
4 return
5 }
6 return Array.from(new Set(arr))
7}
使用 lodash 的 uniq()
1_.uniq([2, 1, 2]); // => [2, 1]
浅拷贝和深拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
在JavaScript中,存在浅拷贝的现象有:
- Object.assign
- Array.prototype.slice(), Array.prototype.concat()
- 使用拓展运算符实现的复制
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
常见的深拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
_.cloneDeep()
JSON.stringify()
但是这种方式存在弊端,会忽略undefined、symbol和函数
vue2和vue3diff算法
虚拟DOM就是用来表示真实dom的对象,vue通过模版编译生成虚拟DOM树,然后在通过渲染器渲染成真实DOM,当数据更新时,产生新的虚拟dom树,如果直接用新的虚拟DOM树生成真实DOM并不是最优的方法。为了进一步降低找出差异的性能的性能消耗,就要使用diff算法。Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,实现精准地更新真实DOM。
1.diff算法diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
2.diff算法就是用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到页面中。
3.当中当状态变更的时候,diff算法又会重新执行,构造一棵新的虚拟DOM树结构。然后用新的树和旧的树进行比较,记录两棵树差异,同时把新的DOM树所记录的差异应用到旧的DOM树中,然后再构建的真正的DOM树上,实现了视图的更新
vue2 双端diff算法的实现
vue2采用了双端diff算法。核心方法是updateChildren,通过新前与旧前、新后与旧后、新后与旧前、新前与旧后、暴力比对5种查找。
新前:newChildren中所有未处理的第一个节点
新后:newChildren中所有未处理的最后一个节点
旧前:oldChildren中所有未处理的第一个节点
旧后:oldChildren中所有未处理的最后一个节点
如果节点比对的时候上面4种方法都不适用时,此时我们只能用最暴力的方法,首先我们需要循环oldChildren生成一个key和index的映射表{‘a’: 0, ‘b’: 1},然后我们用新的开始节点的key,去映射表中查找,如果找到就把该节点移动到最前面,且原来的位置用undefined占位,避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置,如果没有找到就直接把新节点插入
在具体介绍前我们还需要了解isSameVnode这个用来对比两个节点是否相同的方法
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用 function isSameVnode(oldVnode, newVnode) { return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key; }
vue3 快速diff算法的实现
vue3 使用了快速diff算法,核心方法是patchKeyedChildren,首先是借鉴了纯文本diff算法中的预处理思路,处理新旧两个组子节点中相同的前置节点和后置节点。处理完后,如果剩余节点无法简单的通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构建出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
你都做过哪些Vue的性能优化?(问的真多)
(1)代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
高度坍塌
1.为父元素设置overflow:hidden属性。
2.在父元素类的结尾追加一个空子元素(块级元素),并设置空子元素清楚浮动影响(clear:both)
3.设置父元素也浮动。并给后续元素添加clear:both属性
4.为父元素末尾伪元素设置clear:both。–完美解决。
obj构造函数的构造函数指向谁
HTML5新特性
1 语义化标签
清晰易读
有利于seo,方便搜索引擎识别页面结构
方便设备解析 比如盲人阅读
2 表单功能增强
3 音视频标签
4 画布 - Canvas+ SVG
HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。
画布是一个矩形区域,您可以控制其每一像素。
canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
这个后面会专门出一期和svg的对比。
5 拖放
6 本地存储
通过本地存储(Local Storage),web 应用程序能够在用户浏览器中对数据进行本地的存储。
在 HTML5 之前,应用程序数据只能存储在 cookie 中,包括每个服务器请求。本地存储则更安全,并且可在不影响网站性能的前提下将大量数据存储于本地。
与 cookie 不同,存储限制要大得多(至少5MB),并且信息不会被传输到服务器。
本地存储经由起源地(origin)(经由域和协议)。所有页面,从起源地,能够存储和访问相同的数据。
7 Web Worker
当在 HTML 页面中执行脚本时,页面是不可响应的,直到脚本已完成。
Web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 运行在后台。
8 地理定位
HTML5 Geolocation API 用于获得用户的地理位置。
鉴于该特性可能侵犯用户的隐私,除非用户同意,否则用户位置信息是不可用的。
datalist
只不过我们经常使用别人已经封装好的 UI 组件,所以就没怎么用过,此标签就是 HTML5 封装的 Select API。
可编辑内容
它被广泛的应用,比如很多网页编辑器,内容切换编辑状态等等,都可以通过这个属性来实现。
em/px/rem/vh/vw区别
px
px,表示像素,所谓像素就是呈现在我们显示器上的一个个小点,每个像素点都是大小等同的,所以像素为计量单位被分在了绝对长度单位中
有些人会把px认为是相对长度,原因在于在移动端中存在设备像素比,px实际显示的大小是不确定的
这里之所以认为px为绝对单位,在于px的大小和元素的其他属性无关
#em
em是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(1em = 16px)
为了简化 font-size 的换算,我们需要在css中的 body 选择器中声明font-size= 62.5%,这就使 em 值变为 16px*62.5% = 10px
这样 12px = 1.2em, 10px = 1em, 也就是说只需要将你的原来的px 数值除以 10,然后换上 em作为单位就行了
特点:
- em 的值并不是固定的
- em 会继承父级元素的字体大小
- em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸
- 任意浏览器的默认字体高都是 16px
rem
rem,相对单位,相对的只是HTML根元素font-size的值
同理,如果想要简化font-size的转化,我们可以在根元素html中加入font-size: 62.5%
html {font-size: 62.5%; } /* 公式16px*62.5%=10px */
这样页面中1rem=10px、1.2rem=12px、1.4rem=14px、1.6rem=16px;使得视觉、使用、书写都得到了极大的帮助
特点:
- rem单位可谓集相对大小和绝对大小的优点于一身
- 和em不同的是rem总是相对于根元素,而不像em一样使用级联的方式来计算尺寸
#vh、vw
vw ,就是根据窗口的宽度,分成100等份,100vw就表示满宽,50vw就表示一半宽。(vw 始终是针对窗口的宽),同理,<font style="color:rgb(71, 101, 130);">vh</font>
则为窗口的高度
这里的窗口分成几种情况:
- 在桌面端,指的是浏览器的可视区域
- 移动端指的就是布局视口
像<font style="color:rgb(71, 101, 130);">vw</font>
、<font style="color:rgb(71, 101, 130);">vh</font>
,比较容易混淆的一个单位是<font style="color:rgb(71, 101, 130);">%</font>
,不过百分比宽泛的讲是相对于父元素:
- 对于普通定位元素就是我们理解的父元素
- 对于position: absolute;的元素是相对于已定位的父元素
- 对于position: fixed;的元素是相对于 ViewPort(可视窗口)
#三、总结
px:绝对单位,页面按精确像素展示
em:相对单位,基准点为父节点字体的大小,如果自身定义了<font style="color:rgb(71, 101, 130);">font-size</font>
按自身来计算,整个页面内<font style="color:rgb(71, 101, 130);">1em</font>
不是一个固定的值
rem:相对单位,可理解为<font style="color:rgb(71, 101, 130);">root em</font>
, 相对根节点<font style="color:rgb(71, 101, 130);">html</font>
的字体大小来计算
vh、vw:主要用于页面视口大小布局,在页面布局上更加方便简单
数据的类型
基本类型主要为以下7种:
- Number
- String
- Boolean
- Undefined
- null
- symbol
- bigint
引用类型 (复杂类型)统称为Object,我们这里主要讲述下面三种:
- Object
- Array
- Function
声明变量时不同的内存地址分配:
1- <font style="color:rgb(44, 62, 80);">简单类型的值存放在栈中,在栈中存放的是对应的值</font>
2- <font style="color:rgb(44, 62, 80);">引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址</font>
不同的类型数据导致赋值变量时的不同:
1- <font style="color:rgb(44, 62, 80);">简单类型赋值,是生成相同的值,两个对象对应不同的地址</font>
2- <font style="color:rgb(44, 62, 80);">复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象</font>
跨域以及解决方法
什么是跨越
跨域本质是浏览器基于同源策略的一种安全手段
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源(即指在同一个域)具有以下三个相同点
- 协议相同(protocol)
- 主机相同(host)
- 端口相同(port)
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域
一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。
解决方案
Vue-Router 的导航守卫和执行流程
- 触发进入其他路由。
- 调用要离开路由的组件守卫 beforeRouteLeave
- 调用全局前置守卫:beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter 组件守卫
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发DOM更新 (mounted)。
- 执行 beforeRouteEnter 守卫中传给 next 的回调函数
vuex怎样调用action方法
为什么Vuex要通过mutations修改state,而不能直接修改
因为state是实时更新的,mutations无法进行异步操作,而如果直接修改state的话是能够异步操作的,当你异步对state进行操作时,还没执行完,这时候如果state已经在其他地方被修改了,这样就会导致程序存在问题了。所以state要同步操作,通过mutations的方式限制了不允许异步。
vuex和pinia的区别
“Vuex 和 Pinia 都是 Vue 的状态管理工具,但 Pinia 是 Vue 3 推荐的状态管理库,Pinia 相较于 Vuex 有一些显著的优势。以下是它们的主要区别:
- 版本支持:
- Vuex 是 Vue 2.x 的状态管理工具,但在 Vue 3 中也可以使用,不过它是 Vue 2.x 的官方推荐工具。
- Pinia 是 Vue 3 的状态管理库,它是基于 Vue 3 的 Composition API 设计的,官方推荐在 Vue 3 项目中使用。
- API 风格:
- Vuex 使用的是基于
state
,mutations
,actions
,getters
的结构,它遵循的是传统的 Vue 2.x 的 API 风格。 - Pinia 基于 Vue 3 的 Composition API,它使用
defineStore
来定义状态,并且简化了actions
和mutations
的概念。状态、计算属性和方法都可以直接在store
中定义,更符合 Vue 3 的设计理念。
- Vuex 使用的是基于
- 响应性:
- Vuex 在 Vue 3 中使用的是
Vue.observable()
来管理响应性,它与 Vue 3 的 Composition API 不完全兼容。 - Pinia 充分利用了 Vue 3 的响应性系统,所有的状态都是响应式的,而且它可以直接与 Vue 3 的 Composition API 结合,提供更好的性能和开发体验。
- Vuex 在 Vue 3 中使用的是
- 类型支持:
- Vuex 对 TypeScript 的支持较为有限,配置较为繁琐。
- Pinia 内建了对 TypeScript 的良好支持,使用时可以轻松地定义类型和推导,更加简洁和直观。
- 开发工具:
- Vuex 的开发工具相对较为传统,功能比较基础。
- Pinia 提供了更强大的开发者工具,支持时间旅行调试,状态快照等,开发体验更加友好。
- 性能:
- Pinia 在性能上比 Vuex 更优,因为它利用了 Vue 3 的响应式系统,并且经过优化,减少了不必要的计算。
- 模块化:
- Vuex 支持模块化,但其模块化设计相对复杂,需要手动处理命名空间等。
- Pinia 提供了更加简洁的模块化方式,模块间的共享和访问更加方便。
总结:“Pinia 是专为 Vue 3 设计的,提供了更现代、简洁、响应式、且具有更好类型支持的状态管理方式。相比 Vuex,Pinia 更符合 Vue 3 的 Composition API,并且提供更好的性能和开发者体验。”
Pinia 相比 Vuex 舍弃了什么?
Pinia 相比 Vuex 主要舍弃了以下几个复杂概念,简化了开发体验:
- Mutation 被移除:
- 在 Vuex 中,状态必须通过
mutation
修改,而 Pinia **直接修改 ****state**
,不需要额外的mutation
,简化了代码。
- 在 Vuex 中,状态必须通过
- Modules 被移除,改用多个 Store:
- Vuex 需要
modules
来拆分 Store,而 Pinia 直接用**defineStore**
组织多个 Store,代码更清晰。
- Vuex 需要
- mapState、mapGetters 这些辅助函数被优化:
- Pinia 直接支持
**storeToRefs**
,更符合 Vue 3 的 Composition API。
- Pinia 直接支持
- Getters 不能传参:
- 在 Vuex 里,
getters
可以接受参数,而 Pinia 只支持返回值固定的getters
,如果需要动态参数,建议使用actions
。
- 在 Vuex 里,
- **this.$store 被移除,改用 **
**useStore**
:- Vuex 依赖
this.$store
,而 Pinia 采用**useStore()**
直接获取 Store,更符合 Vue 3 设计。
- Vuex 依赖
总结
“Pinia 舍弃了 Vuex 的 mutation
、modules
和复杂的辅助函数,让状态管理更简单,和 Vue 3 组合式 API 更契合。”
强缓存和协商缓存以及具体过程
“强缓存和协商缓存都是浏览器缓存机制的两种方式,用于提高网页的加载速度和减少服务器的压力。它们的区别主要在于缓存的控制方式和缓存命中的条件。下面我会详细讲解它们的区别以及具体的工作过程。”
1. 强缓存 (Strong Cache)
强缓存指的是当浏览器直接从缓存中读取资源,不去向服务器发起请求。浏览器会根据资源的缓存控制策略,决定是否使用缓存。
- 使用方式:
- 强缓存通过
Cache-Control
和Expires
两个 HTTP 头部进行控制。 - Cache-Control:是现代浏览器使用的控制缓存的方式,通常设置为
max-age
,表示资源的最大缓存时间。 - Expires:是 HTTP/1.0 的缓存头,表示缓存的过期时间,是一个具体的时间戳。
- 强缓存通过
- 过程:
- 当用户访问一个页面时,浏览器会根据资源的
Cache-Control
或Expires
的设置,直接从缓存中获取资源,而不会请求服务器。 - 如果资源没有过期,浏览器会直接使用缓存,不会向服务器发起请求,节省了带宽和时间。
- 当用户访问一个页面时,浏览器会根据资源的
2. 协商缓存 (Conditional Cache)
协商缓存是指浏览器会向服务器发送请求,询问缓存是否有效,服务器根据请求头返回是否使用缓存。
- 使用方式:
- 协商缓存通过
Last-Modified
和ETag
这两个 HTTP 头部进行控制。 - Last-Modified:表示资源的最后修改时间,浏览器会在请求时携带
If-Modified-Since
头部,向服务器询问自某个时间以来资源是否发生变化。 - ETag:是资源的唯一标识符,通常是资源内容的 hash 值,浏览器会发送
If-None-Match
头部,询问服务器资源的 ETag 是否与缓存一致。
- 协商缓存通过
- 过程:
- 浏览器首次请求资源时,会携带
Last-Modified
或ETag
信息。 - 服务器根据这些信息判断资源是否发生变化:
- 如果资源没有变化,服务器返回
304 Not Modified
,浏览器继续使用本地缓存。 - 如果资源变化,服务器返回最新的资源,并更新缓存。
- 如果资源没有变化,服务器返回
- 浏览器首次请求资源时,会携带
3. 强缓存和协商缓存的区别
特性 | 强缓存 (Strong Cache) | 协商缓存 (Conditional Cache) |
---|---|---|
定义 | 资源未过期时,浏览器直接使用缓存,而不与服务器进行任何交互 | 浏览器发起请求,服务器根据缓存的有效性决定是否返回缓存内容 |
控制方式 | Cache-Control 、 Expires | Last-Modified 、 ETag |
缓存命中条件 | 只要缓存未过期,直接使用缓存 | 缓存过期后,通过与服务器协商决定是否返回缓存内容 |
性能 | 提升性能,减少请求次数,但失效时需要重新加载资源 | 缓存过期时,增加了与服务器的交互,但可以保证数据的有效性 |
4. 具体过程总结
- 强缓存:当资源没有过期时,浏览器直接从缓存读取资源,不会与服务器交互。
- 协商缓存:当资源过期时,浏览器会向服务器发送请求,通过
Last-Modified
或ETag
与服务器进行协商,判断缓存是否有效。如果有效,返回304 Not Modified
,否则返回最新的资源。
总结:
“强缓存直接从缓存中获取资源,避免向服务器发起请求;而协商缓存则会向服务器询问资源是否有更新,确保获取到最新的资源。两者配合使用,可以有效提高页面加载速度,减少服务器压力。”
element-plus和element-ui有哪些不同
Element-ui和Element-Plus的区别_Element2和Element3的区别
一、定义区别
Element-UI对应Element2:基本不支持手机版
Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库
Element-Plus对应Element3:组件布局考虑了手机版展示
基于 Vue 3,面向设计师和开发者的组件库
二、框架区别
Element-ui适用于Vue2框架
Element-plus适用于Vue3框架
三、开发中使用的区别
1. Icon图标库变化了
新版本的图标库使用方式
2.组件的插槽slot使用变化了
同时可支持多个插槽
1<el-autocomplete popper-class="my-autocomplete" v-model="state" :fetch-suggestions="querySearch" placeholder="请输入内容" @select="handleSelect" >
2 <template #suffix>
3 <i class="el-icon-edit el-input__icon" @click="handleIconClick"> </i>
4 </template>
5 <template #default="{ item }">
6 <div class="name">{{ item.value }}</div>
7 <span class="addr">{{ item.address }}</span>
8 </template>
9 </el-autocomplete>
3.新增组件
- Skeleton-骨架屏
- Empty-空状态
- Affix -固钉
- TimeSelect 时间选择
- Space 间距
数据持久化存储
vue中缓存
vue实现缓存有4种方式:1、利用localStorage;2、利用sessionStorage;3、安装并引用storage.js插件;4、利用vuex,它是一个专为Vue.js应用程序开发的状态管理模式
storage.js一款基于 localStorage 本地储存的 js 插件 ,提供有效期、只读取一次以及输出 json 数据等功能。
keep-live keep-live组件是vue的一个内置组件,可以实现组件的缓存,当组件切换时,不会对当前组件进行卸载。
- 常用的两个属性include/exclude,允许组件进行有条件的进行缓存;
- 常用的两个生命周期activated/deactivated,根据当前组件的活跃状态来触发;
- keep-live中还运用了LRU算法,选择最久未使用的组件予以淘汰。
浏览器兼容性问题
1. 为什么会有浏览器兼容性问题?
不同浏览器(Chrome、Firefox、Safari、Edge、IE)对HTML、CSS、JavaScript 解析标准的实现不同,尤其是老旧浏览器(如 IE)对现代 Web 技术的支持有限,导致兼容性问题。
2. 常见的浏览器兼容性问题及解决方案
问题 | 原因 | 解决方案 |
---|---|---|
CSS 不兼容 | flex 、 grid 、 position: sticky 在部分浏览器支持不同 | 使用 autoprefixer 添加前缀,或者使用 can i use 查询兼容性 |
JS 语法兼容问题 | ES6+ 语法(let/const 、 Promise 、 async/await )在旧版浏览器不支持 | 使用 Babel 进行 ES6+ 代码转换 |
事件兼容问题 | addEventListener 在 IE8 及以下不支持 | 使用 attachEvent 处理,或者通过 document.onclick 兼容 |
Cookie 不兼容 | SameSite 属性在某些浏览器要求严格 | 设置 SameSite=None; Secure 以适配不同环境 |
跨域问题 | 不同浏览器对 CORS 处理不同 | 服务器配置 Access-Control-Allow-Origin ,或者使用 JSONP 兼容老旧浏览器 |
**localStorage** ** 兼容性** | IE7 及以下不支持 | 使用 cookie 代替,或者 try-catch 进行降级处理 |
视频/音频格式 | 不同浏览器对 mp4 、 webm 、 ogg 支持不同 | 提供多个格式的资源,如 <source src="video.mp4"> <source src="video.webm"> |
Placeholder 兼容性 | 早期 IE 版本不支持 placeholder | 使用 JavaScript 监听 focus 和 blur 模拟 |
3. 兼容性优化策略
- 渐进增强(Progressive Enhancement):先实现基本功能,针对支持新特性的浏览器进行优化。
- 优雅降级(Graceful Degradation):先开发完整功能,再对不支持的浏览器提供降级方案。
- Polyfill/Shim:使用
core-js
、@babel/polyfill
让旧版浏览器支持新特性。 - 前缀处理:使用
PostCSS
或Autoprefixer
添加浏览器前缀(-webkit-
、-moz-
)。 - 特性检测:用
Modernizr
检测浏览器支持情况,决定是否使用某些功能。 - 查询兼容性:使用
Can I use
(https://caniuse.com)查询各浏览器支持情况。
总结
浏览器兼容性问题主要来自于CSS 样式、JS 语法、存储、跨域、事件处理等方面。
通过Babel 转译、Polyfill 补丁、渐进增强策略等方式可以提高 Web 兼容性,使其在不同浏览器中正常运行。
SPA
- SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序,简单通俗点就是在一个项目中只有一个html页面,它在第一次加载页面时,将唯一完成的html页面和所有其余页面组件一起下载下来,所有的组件的展示与切换都在这唯一的页面中完成,这样切换页面时,不会重新加载整个页面,而是通过路由来实现不同组件之间的切换。
- 单页面应用(SPA)的核心之一是:更新视图而不重新请求页面。
- 优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
- 缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
实现AJAX
实现 <font style="color:rgb(71, 101, 130);">Ajax</font>
异步交互需要服务器逻辑进行配合,需要完成以下步骤:
- 创建
<font style="color:rgb(71, 101, 130);">Ajax</font>
的核心对象<font style="color:rgb(71, 101, 130);">XMLHttpRequest</font>
对象 - 通过
<font style="color:rgb(71, 101, 130);">XMLHttpRequest</font>
对象的<font style="color:rgb(71, 101, 130);">open()</font>
方法与服务端建立连接 - 构建请求所需的数据内容,并通过
<font style="color:rgb(71, 101, 130);">XMLHttpRequest</font>
对象的<font style="color:rgb(71, 101, 130);">send()</font>
方法发送给服务器端 - 通过
<font style="color:rgb(71, 101, 130);">XMLHttpRequest</font>
对象提供的<font style="color:rgb(71, 101, 130);">onreadystatechange</font>
事件监听服务器端你的通信状态 - 接受并处理服务端向客户端响应的数据结果
- 将处理结果更新到
<font style="color:rgb(71, 101, 130);">HTML</font>
页面中
javaScript中判断是否为整数的方法
方式一、使用取余运算符判断
思路: 利用任何整数都会被1整除,即余数是0的特定,通过这个规则来判断是否是整数。
方式二、使用Math.round、Math.ceil、Math.floor判断
思路: 整数取整后还是等于自己。利用这个特性来判断是否是整数
方式三、通过parseInt()判断
思路: 利用parseInt()十进制的转化特点
for of 和for in得区别
for...in
和 for...of
都是遍历的方法,但适用的场景不同。
**for...in**
用于遍历对象的可枚举属性,它遍历的是键名,而不仅仅是对象自身的属性,原型链上的可枚举属性也会被遍历到。所以一般用于遍历对象,但不适用于数组,因为可能会遍历出不想要的属性。**for...of**
主要用于遍历可迭代对象,比如数组、字符串、Set、Map 等。它遍历的是值,不会遍历原型链上的属性,更适合遍历数组和集合。
总结:遍历对象用 for...in
,遍历数组、字符串、Set、Map 用 for...of
。
forEach和map的区别
面试口头回答(详细版):
forEach
只是对数组的每个元素执行操作,不会返回新的数组,而 map
会返回一个新的数组,数组中的每个元素都是回调函数返回的结果。map
不会修改原数组,而是基于回调函数的返回值创建一个新数组。
如果不关心返回值,比如打印日志、修改 DOM,可以用 forEach
。如果需要一个新数组,比如对数组元素做转换,可以用 map
。
map 不会改变原数组吗?
map
不会修改原数组的结构(即数组的长度、索引不变),但如果数组元素是引用类型(如对象),修改对象的属性仍会影响原数组。
数组中有哪些方法会修改原数组,那些不会
http三次握手
- 第一步:客户端发送SYN报文到服务端发起握手,发送完之后客户端处于SYN_Send状态
- 第二步:服务端收到SYN报文之后回复SYN和ACK报文给客户端
- 第三步:客户端收到SYN和ACK,向服务端发送一个ACK报文,客户端转为established状态,此时服务端收到ACK报文后也处于established状态,此时双方已建立了连接
http四次挥手
刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
- 服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
Vue 父子组件的生命周期执行顺序?
面试官可能会问:
- Vue 组件的生命周期钩子在父子组件中的执行顺序是怎样的?
- 为什么 Vue 会按照这个顺序执行?
- 如果子组件销毁,生命周期钩子的执行顺序是怎样的?
回答:Vue 组件的生命周期执行顺序
在 Vue 组件的生命周期中,父组件的钩子会先执行,但在涉及到子组件挂载或销毁时,子组件的钩子会先执行。 具体的执行顺序如下:
1. 组件创建(挂载时)
当一个父组件创建并挂载一个子组件时,生命周期的执行顺序如下:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
🔹 解释:
- 父组件的 beforeCreate 和 created 先执行,因为父组件要先解析模板,才能发现需要挂载子组件。
- 父组件执行 beforeMount 后,开始挂载子组件,这时子组件开始执行 beforeCreate、created、beforeMount 和 mounted。
- 子组件 mounted 执行完后,父组件 mounted 才执行,因为子组件需要先完成渲染。
2. 组件更新时
当父组件的状态发生变化,导致子组件需要重新渲染时,生命周期的执行顺序如下:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
🔹 解释:
- 父组件数据变化,进入 beforeUpdate 阶段,但由于子组件依赖父组件的数据,子组件也需要更新。
- 子组件的 beforeUpdate 先执行,然后子组件更新完成后执行 updated。
- 子组件更新完毕后,父组件才更新,因此最后执行父组件的 updated。
3. 组件销毁(卸载时)
当父组件销毁时,生命周期的执行顺序如下:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destroyed
🔹 解释:
- 父组件 beforeDestroy 先执行,但它不会立即销毁自身,而是先去销毁子组件。
- 子组件的 beforeDestroy 执行后,开始执行 destroyed,子组件销毁完成后,才轮到父组件的 destroyed。
总结
阶段 | 父组件执行的生命周期 | 子组件执行的生命周期 |
---|---|---|
挂载 | beforeCreate → created → beforeMount | beforeCreate → created → beforeMount → mounted |
更新 | beforeUpdate → updated | beforeUpdate → updated |
销毁 | beforeDestroy → destroyed | beforeDestroy → destroyed |
面试官可能追问
- 为什么在挂载时,子组件的 mounted 先执行,然后才是父组件的 mounted?
- 因为 Vue 需要确保子组件完全渲染完成,父组件才能算挂载成功。
- 如果父组件
**beforeDestroy**
抛出异常,子组件还会销毁吗?- 可能会导致子组件销毁不完全,因此在
beforeDestroy
里最好不要进行复杂操作或阻塞逻辑。
- 可能会导致子组件销毁不完全,因此在
**beforeDestroy**
** 里可以获取 DOM 吗?**- 可以,但 DOM 可能即将被销毁,使用时要谨慎。
最终总结
“在 Vue 组件生命周期中,父组件的 beforeCreate、created 先执行,然后才轮到子组件。在挂载过程中,子组件 mounted 先执行,再到父组件 mounted。在销毁时,父组件先进入 beforeDestroy,但会优先销毁子组件,子组件 destroyed 完成后,父组件才会执行 destroyed。”
vue项目移动端、pc端适配方案
export和export default的区别
- export与export default均可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要,并可以起任意名称
- (1) 输出单个值,使用export default
- (2) 输出多个值,使用export
- (3) export default与普通的export不要同时使用
有没有了解过webpack
是的,我熟悉 Webpack,它是一个 前端资源打包工具,主要用于将 JavaScript、CSS、图片等资源进行模块化管理和优化,最终输出适合生产环境的文件。
1. Webpack 的核心概念
Webpack 主要由 五个核心概念 组成:
- Entry(入口)
- 指定 Webpack 打包的起点,例如:
1module.exports = { entry: './src/index.js' };
- Output(输出)
- 指定打包后的文件位置,例如:
1module.exports = {
2 output: {
3 filename: 'bundle.js',
4 path: __dirname + '/dist'
5 }
6};
- Loaders(加载器)
- 让 Webpack 处理 非 JavaScript 文件(如 CSS、图片、TypeScript)。
- 例如:
babel-loader
处理 ES6+ 代码,css-loader
处理 CSS。
- Plugins(插件)
- 扩展 Webpack 功能,比如压缩代码、提取 CSS、优化性能等。
- 例如:
HtmlWebpackPlugin
生成 HTML 文件,MiniCssExtractPlugin
提取 CSS 文件。
- Mode(模式)
- Webpack 有 开发模式(development) 和 生产模式(production):
1mode: 'development' // 或 'production'
1- 生产模式会自动优化代码,如 Tree Shaking、代码压缩等。
2. Webpack 的主要功能
(1)模块化支持
- Webpack 支持 CommonJS、ESModule、AMD、TS 等多种模块化方案。
(2)代码拆分(Code Splitting)
- 通过
SplitChunksPlugin
和import()
进行按需加载,减少首屏加载时间。
(3)Tree Shaking(去除无用代码)
- 依赖 ES6
import/export
,移除未使用的代码,减少打包体积。
(4)热更新(HMR - Hot Module Replacement)
- 监听代码变化,实时更新浏览器,无需刷新页面。
(5)多种优化策略
- 例如 缓存优化(contenthash)、懒加载(lazy loading)、压缩优化(TerserPlugin) 等。
3. Webpack 优化点(高级)
如果面试官深入问 Webpack 优化,你可以提到这些:
优化方向 | 方法 |
---|---|
体积优化 | Tree Shaking、Scope Hoisting、按需加载 |
速度优化 | 开启多线程 thread-loader 、 cache-loader |
图片优化 | image-webpack-loader 、 url-loader |
缓存优化 | contenthash 生成稳定缓存文件 |
构建优化 | webpack-bundle-analyzer 进行分析 |
4. Webpack 和 Vite、Rollup 对比
如果面试官问 Webpack 和其他工具的区别,你可以简要比较:
- Webpack:功能全面,适用于大型项目,但构建速度较慢。
- Vite:基于 ESModules 和
esbuild
,开发体验更好,适用于 Vue/React 项目。 - Rollup:更适合构建 库(Library),对 Tree Shaking 支持更好。
5. 面试高频问题
- Webpack 的 Loader 和 Plugin 有什么区别?
- Loader 用于 转换文件(如
babel-loader
处理 JS,css-loader
处理 CSS)。 - Plugin 用于 扩展功能(如
HtmlWebpackPlugin
生成 HTML)。
- Loader 用于 转换文件(如
- Tree Shaking 的原理是什么?
- 依赖 ES6 的
import/export
语法,删除未使用的代码,减少打包体积。
- 依赖 ES6 的
- 如何提高 Webpack 构建速度?
- 开启
cache-loader
、使用thread-loader
多线程、减少resolve
解析路径。
- 开启
总结
Webpack 是前端工程化的重要工具,具备 模块化管理、代码拆分、优化构建 等功能。在优化 Webpack 方面,可以从 体积、构建速度、缓存 等角度入手,结合 Vite、Rollup 选择合适的工具。
webpack打包流程
- 初始化参数。获取用户在webpack.config.js文件配置的参数
- 开始编译。初始化compiler对象,注册所有的插件plugins,插件开始监听webpack构建过程的生命周期事件,不同环节会有相应的处理,然后开始执行编译。
- 确定入口。根据webpack.config.js文件的entry入口,开始解析文件构建ast语法树,找抽依赖,递归下去。
- 编译模块。递归过程中,根据文件类型和loader配置,调用相应的loader对不同的文件做转换处理,在找出该模块依赖的模块,递归本操作,直到项目中依赖的所有模块都经过了本操作的编译处理。
- 完成编译并输出。递归结束,得到每个文件结果,包含转换后的模块以及他们之前的依赖关系,根据entry以及output等配置生成代码块chunk
- 打包完成。根据output输出所有的chunk到相应的文件目录
如何使用 Webpack 进行前端性能优化?
使用 Webpack 进行前端性能优化主要可以通过以下几个方面:
1) 代码分割:通过动态导入或入口配置,将应用拆分成多个小块,按需加载,提高首次加载速度。
2) 资源压缩:使用 TerserWebpackPlugin 对 JavaScript 进行压缩,使用 css-minimizer-webpack-plugin 压缩 CSS,减少文件体积。
3) 图片优化:使用 image-webpack-loader 压缩图片,降低加载时间,改善用户体验。
4) 预加载和预取:使用 Webpack 的 <font style="color:rgb(31, 35, 40);">webpackPrefetch</font>
和 <font style="color:rgb(31, 35, 40);">webpackPreload</font>
提高资源加载效率。
5) 缓存管理:设置合适的缓存策略,通过 hash 文件名管理缓存,避免用户下载过期资源。
6) Tree Shaking:通过 ES6 模块的静态分析,去除未使用的代码,减小打包后的体积。
常见的 Webpack Loader 有哪些?
Webpack Loader 是一种转换器,用于将不同类型的文件转换为可以被 Webpack 处理的模块。常见的 Webpack Loader 有以下几种:
1)babel-loader:
用于将 ES6+ 代码转换为向后兼容的 JavaScript 代码,以支持旧版浏览器。
2)css-loader 和 style-loader:
css-loader:解析 CSS 文件中的 @import 和 url(),并将其转换为模块;style-loader:将 CSS 注入到 DOM 中的 style 标签中。这两个 loader 在项目中通常会同时进行使用。
3)file-loader:
用于处理文件(如图片、字体等),并将这些文件发送到输出目录,返回文件的 URL。
4)url-loader:
类似于 file-loader,但在文件小于设定的字节限制时,返回 base64 编码的 Data URL。
5)sass-loader:
用于将 Sass/SCSS 文件编译为 CSS 文件。
6)ts-loader:
用于将 TypeScript 代码转换为 JavaScript。
常见的 Webpack Plugin 有哪些?
回答重点
常见的 Webpack 插件包括 HtmlWebpackPlugin、CleanWebpackPlugin、MiniCssExtractPlugin、TerserPlugin、DefinePlugin 和 HotModuleReplacementPlugin 等。这些插件提供了从自动生成 HTML 文件到优化和压缩代码、提取 CSS、定义环境变量和实现模块热替换等多种功能。
扩展知识
1)HtmlWebpackPlugin
HtmlWebpackPlugin 用于自动生成 HTML 文件,并将打包后的 JavaScript 和 CSS 文件自动注入到 HTML 中。这对于单页面应用程序(SPA)非常有用。
1const HtmlWebpackPlugin = require('html-webpack-plugin');
2
3module.exports = {
4 plugins: [
5 new HtmlWebpackPlugin({
6 template: './src/index.html'
7 })
8 ]
9}
2)CleanWebpackPlugin
CleanWebpackPlugin 用于在每次构建前清理输出目录(如 dist 目录),确保生成的文件是最新的。
1const { CleanWebpackPlugin } = require('clean-webpack-plugin');
2
3module.exports = {
4 plugins: [
5 new CleanWebpackPlugin()
6 ]
7}
3)MiniCssExtractPlugin
MiniCssExtractPlugin 用于将 CSS 从 JavaScript 文件中提取到单独的 CSS 文件中。这有助于更好地缓存 CSS 文件并提高页面加载速度。
1const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2
3module.exports = {
4 plugins: [
5 new MiniCssExtractPlugin({
6 filename: '[name].[contenthash].css'
7 })
8 ]
9}
4)TerserPlugin
TerserPlugin 是 Webpack 默认的 JavaScript 压缩工具,用于在生产模式下压缩和优化 JavaScript 文件。
5)DefinePlugin
DefinePlugin 用于创建在编译时可以配置的全局常量。这对于根据环境变量配置应用程序非常有用。
1const webpack = require('webpack');
2
3module.exports = {
4 plugins: [
5 new webpack.DefinePlugin({
6 'process.env.NODE_ENV': JSON.stringify('production')
7 })
8 ]
9}
6)HotModuleReplacementPlugin
HotModuleReplacementPlugin 用于启用模块热替换(HMR),允许在不刷新整个页面的情况下替换、添加或删除模块。这对于开发时提高效率和用户体验非常有帮助。
1const webpack = require('webpack');
2
3module.exports = {
4 plugins: [
5 new webpack.HotModuleReplacementPlugin()
6 ]
7}
7)BundleAnalyzerPlugin
BundleAnalyzerPlugin 用于可视化分析打包后的文件体积,帮助开发者识别和优化大文件和重复模块。
Webpack 和 Vite 有什么区别?
1. 核心区别:
- Webpack 采用 “打包后运行” 方式,需要把所有模块先编译打包,再在浏览器中执行。
- Vite 采用 “按需加载” 方式,开发环境直接使用浏览器的 ES Modules(ESM),不需要预打包,启动更快。
2. 开发体验(HMR 热更新 & 启动速度):
- Webpack 启动慢,因为它需要完整编译整个项目,即使修改一个文件,也要重新打包,HMR 速度会随着项目变大变慢。
- Vite 即开即用,因为它不需要提前打包,修改代码后浏览器直接加载修改的模块,HMR 速度极快。
3. 生产构建(优化 & Tree Shaking):
- Webpack 使用 AST 分析,支持 Code Splitting(代码拆分) 和 Tree Shaking,但优化依赖于配置。
- Vite 生产环境基于 Rollup 打包,默认支持 更细粒度的代码拆分,优化效果更好。
4. 适用场景:
- Webpack 适用于 大型复杂项目(如企业级应用、SSR)。
- Vite 适用于 Vue 3 / React 现代前端开发,更适合 中小型项目 或 前端开发阶段。
总结:
- 开发阶段:Vite 体验更流畅,Webpack 需要等待打包。
- 生产阶段:Webpack 生态更成熟,Vite 基于 Rollup 也能优化良好。
- 选择建议:如果是 现代前端项目(Vue 3 / React),建议用 Vite,如果是 大型企业应用,Webpack 依然是主流。
回答重点
Webpack 和 Vite 是两种常用的前端构建工具,它们在构建机制、性能和使用场景上有显著区别。
1)构建机制
Webpack:
基于模块打包,使用依赖图构建 需要完整打包,适合大型项目 支持多种模块格式(CommonJS、ESM)
Vite:
基于原生 ES 模块,使用浏览器支持的 import 开发时不打包,使用按需加载 适合现代浏览器,支持 HMR
2)性能
Webpack:
初次构建较慢,增量构建较快 需要配置优化,支持 tree-shaking 适合复杂项目,支持多种优化插件
Vite:
开发启动快,热更新速度快 构建速度快,使用 Rollup 打包 适合现代项目,支持 ES6+ 特性
扩展知识
1)Webpack 的优缺点
优点
1)功能强大:支持复杂的项目构建,提供丰富的插件和 loader 生态。
2)高度可配置:可以通过配置文件自定义构建流程,满足各种需求。
3)广泛使用:社区活跃,文档丰富,适合大型项目。
缺点
1)配置复杂:对于新手来说,配置文件可能比较难以理解。
2)构建速度:在大型项目中,初次构建和热更新速度较慢。
3)学习曲线:需要一定的学习成本来掌握其配置和优化技巧。
2)Vite 的优缺点
优点
1)快速启动:利用浏览器的原生 ES 模块支持,开发环境启动速度极快。
2)即时热更新:基于 ES 模块的热更新机制,提供了流畅的开发体验。
3)简单配置:默认配置即能满足大部分需求,适合快速开发。
缺点
1)生态相对较新:虽然支持大部分现代框架,但在一些复杂场景下可能需要额外配置。
2)生产构建:虽然 Vite 也支持生产构建,但在某些复杂项目中,可能需要结合其他工具。
3)兼容性:依赖于浏览器的 ES 模块支持,可能在旧浏览器中需要 polyfill。
3)使用场景
Webpack 适用场景
1)大型项目:需要复杂的构建配置和优化。
2)多种资源:需要处理多种类型的资源文件。
3)自定义需求:需要高度自定义的构建流程。
Vite 适用场景
1)快速开发:需要快速启动和热更新的开发环境。
2)现代框架:使用 Vue、React 等现代框架进行开发。
3)小型项目:不需要复杂的构建配置。
4)选择建议
1)如果项目规模较大,且需要复杂的构建配置,Webpack 是一个成熟的选择。
2)如果追求快速开发体验,且项目规模较小,Vite 是一个不错的选择。
3)可以结合使用:在开发阶段使用 Vite 提供快速反馈,在生产阶段使用 Webpack 进行优化构建。
5)配置示例
Webpack 配置示例:
1const path = require('path');
2
3module.exports = {
4 entry: './src/index.js',
5 output: {
6 filename: 'bundle.js',
7 path: path.resolve(__dirname, 'dist')
8 },
9 module: {
10 rules: [
11 {
12 test: /\.css$/,
13 use: ['style-loader', 'css-loader']
14 }
15 ]
16 },
17 plugins: [
18 new HtmlWebpackPlugin({
19 template: './src/index.html'
20 })
21 ]
22};
Vite 配置示例:
1import { defineConfig } from 'vite';
2
3export default defineConfig({
4 root: './src',
5 build: {
6 outDir: '../dist'
7 },
8 server: {
9 port: 3000
10 }
11});
http 和https得区别
- HTTPS是HTTP协议的安全版本,HTTP协议的数据传输是明文的,是不安全的,HTTPS使用了SSL/TLS协议进行了加密处理,相对更安全
- HTTP 和 HTTPS 使用连接方式不同,默认端口也不一样,HTTP是80,HTTPS是443
- HTTPS 由于需要设计加密以及多次握手,性能方面不如 HTTP
- HTTPS需要SSL,SSL 证书需要钱,功能越强大的证书费用越高
Gzip如何开启的
如何理解CDN?说说实现原理?
CDN(内容分发网络,Content Delivery Network)是一种分布式网络架构,主要用于加速内容传输,提高网站或应用的访问速度和稳定性。
CDN的核心目标
- 提高访问速度:减少用户访问的延迟,优化加载时间。
- 降低源站压力:减少直接请求源站的流量,防止因高并发导致的服务器崩溃。
- 提高可用性:通过分布式节点,保证内容的高可用,即使部分服务器故障也不影响整体服务。
CDN的实现原理
- 节点分布
CDN由多个分布在全球各地的边缘服务器(Edge Server)组成,靠近用户的服务器可以快速响应请求。 - 缓存机制
- 当用户第一次访问资源时,请求会被转发到CDN服务器,CDN服务器如果没有缓存,就会向源站拉取数据并缓存起来。
- 下次有用户请求相同资源时,CDN直接返回缓存内容,无需访问源站,提高响应速度。
- DNS解析调度
- CDN使用智能DNS解析,将用户的请求调度到最近的、负载较低的CDN节点,提高访问速度。
- 例如,用户访问
www.example.com
,DNS解析时会返回一个离用户最近的CDN节点IP,而不是源站IP。
- 负载均衡
- 采用负载均衡算法(如轮询、最少连接等)在多个CDN节点之间分配请求,防止单个节点压力过大。
- 内容回源
- 当CDN缓存过期或者没有命中缓存时,会向源站回源获取最新内容,然后再缓存到CDN节点。
- 动静分离
- 静态资源(图片、CSS、JS等)通常由CDN缓存,减少源站压力。
- 动态请求(API、数据库查询)仍然需要回源处理,但可以通过CDN的智能路由优化访问路径。
总结
CDN的核心是通过就近访问、缓存加速、智能调度等手段提高访问效率,减少延迟,降低源站压力,是现代互联网加速和稳定性优化的重要技术之一。
双向通信以及实现 WebSocket
双向通信指的是客户端和服务器之间能够互相发送和接收数据,而不仅仅是单向请求-响应模式。常见的双向通信方式包括:
- WebSocket(最常用,低延迟)
- 通过
ws://
或wss://
协议建立持久连接,实现实时通信。 - 服务器可以主动推送数据,而客户端也可以随时发送数据。
- 通过
- SSE(Server-Sent Events)(适用于服务器单向推送)
- 基于 HTTP,服务器可以持续向客户端发送事件,但客户端不能主动向服务器发送数据。
- 轮询(Polling) & 长轮询(Long Polling)
- 普通轮询:客户端定期发送请求获取数据,消耗带宽较大。
- 长轮询:服务器保持请求直到有新数据再返回,减少无效请求。
在 Vue 中使用 WebSocket 主要分为 创建连接、监听消息、发送消息、关闭连接 四个步骤,具体流程如下:
1. 在 Vue 组件中使用 WebSocket
- 在
created()
生命周期钩子中创建 WebSocket 连接。 - 监听
onmessage
事件,接收服务器发送的数据。 - 通过方法
sendMessage()
发送消息到服务器。 - 在
beforeUnmount()
钩子中关闭 WebSocket 连接,防止内存泄漏。
代码示例:
1<template>
2 <div>
3 <p>收到的消息:{{ message }}</p>
4 <input v-model="inputMsg" placeholder="输入消息">
5 <button @click="sendMessage">发送</button>
6 </div>
7</template>
8
9<script>
10export default {
11 data() {
12 return {
13 socket: null, // WebSocket 实例
14 message: '', // 存储接收的消息
15 inputMsg: '' // 用户输入的消息
16 };
17 },
18 created() {
19 this.socket = new WebSocket('ws://yourserver.com'); // 连接 WebSocket 服务器
20
21 // 监听服务器发送的消息
22 this.socket.onmessage = (event) => {
23 this.message = event.data; // 更新消息
24 };
25
26 // 监听连接成功
27 this.socket.onopen = () => {
28 console.log('WebSocket 连接成功');
29 };
30
31 // 监听连接错误
32 this.socket.onerror = (error) => {
33 console.error('WebSocket 发生错误:', error);
34 };
35
36 // 监听连接关闭
37 this.socket.onclose = () => {
38 console.log('WebSocket 连接已关闭');
39 };
40 },
41 methods: {
42 sendMessage() {
43 if (this.socket && this.socket.readyState === WebSocket.OPEN) {
44 this.socket.send(this.inputMsg); // 发送消息
45 this.inputMsg = ''; // 发送后清空输入框
46 } else {
47 console.error('WebSocket 未连接');
48 }
49 }
50 },
51 beforeUnmount() {
52 if (this.socket) {
53 this.socket.close(); // 组件销毁时关闭 WebSocket 连接
54 }
55 }
56};
57</script>
2. WebSocket 结合 Vuex/Pinia 实现全局管理
如果多个组件需要共享 WebSocket 数据,可以使用 Vuex 或 Pinia 来管理 WebSocket 连接,避免每个组件都创建新的 WebSocket 实例。
Pinia 方式
1// stores/websocket.js
2import { defineStore } from 'pinia';
3
4export const useWebSocketStore = defineStore('websocket', {
5 state: () => ({
6 socket: null,
7 messages: []
8 }),
9 actions: {
10 connect(url) {
11 if (!this.socket) {
12 this.socket = new WebSocket(url);
13
14 this.socket.onmessage = (event) => {
15 this.messages.push(event.data);
16 };
17
18 this.socket.onopen = () => console.log('WebSocket 连接成功');
19 this.socket.onerror = (error) => console.error('WebSocket 错误:', error);
20 this.socket.onclose = () => console.log('WebSocket 连接已关闭');
21 }
22 },
23 sendMessage(msg) {
24 if (this.socket && this.socket.readyState === WebSocket.OPEN) {
25 this.socket.send(msg);
26 }
27 },
28 close() {
29 if (this.socket) {
30 this.socket.close();
31 this.socket = null;
32 }
33 }
34 }
35});
在组件中使用:
1<script setup>
2import { useWebSocketStore } from '@/stores/websocket';
3import { onMounted, onUnmounted } from 'vue';
4
5const wsStore = useWebSocketStore();
6
7onMounted(() => {
8 wsStore.connect('ws://yourserver.com');
9});
10
11onUnmounted(() => {
12 wsStore.close();
13});
14
15const sendMessage = () => {
16 wsStore.sendMessage('Hello WebSocket');
17};
18</script>
总结
- 单个组件使用 WebSocket
- 在
created()
里创建连接,onmessage
监听消息,sendMessage()
发送数据,beforeUnmount()
关闭连接。
- 在
- 多个组件共享 WebSocket
- 使用 Vuex/Pinia 管理 WebSocket 连接,避免重复创建,提高全局管理能力。
- 避免 WebSocket 连接泄漏
- 组件销毁时(
beforeUnmount
)关闭连接,或者全局管理 WebSocket 避免重复实例化。
- 组件销毁时(
TypeScript 有哪些常用类型?
TypeScript 的常用类型包括:
- 基础类型:string、number、boolean、null、undefined、symbol、bigint
- 复杂类型:array、tuple、enum、object
- 特殊类型:any、unknown、never、void
下面分别讲解:
基础类型
- string:表示字符串。例如:let name: string = “John”
- number:表示数字数据,包括整数和浮点数。例如:let age: number = 30
- boolean:表示布尔值,只有 true 和 false 两种取值。例如:let isActive: boolean = true
- null:表示空值,通常与 undefined 一起使用
- undefined:表示未定义的值
- symbol:表示独一无二的值,主要用于对象属性的唯一标识。例如:let sym: symbol = Symbol()
- bigint:表示任意精度的整数。例如 12345678901234567890123456
- 数组 []
- 元组 Tuple
复杂类型
1)array 数组:表示元素类型固定的列表。例如:
1// 1、在元素类型后面接上[],表示由此类型元素组成的一个数组
2let numbers: number[] = [1, 2, 3];
3// 2、使用数组泛型,Array<元素类型>
4let numbers: Array<number> = [1, 2, 3];
2)tuple 元祖:表示已知数量和类型的数组。例如:
1let x: [string, number] = ["hello", 10];
2
3当访问一个已知索引的元素,会得到正确的类型:
4console.log(x[0].substr(1)); // OK
5console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
6
7当访问一个越界的元素,会使用联合类型替代
8x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
9console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
10x[6] = true; // Error, 布尔不是(string | number)类型
3)enum 枚举:用于定义一组命名常量。例如:
enum 类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
1enum Color {
2 Red,
3 Green,
4 Blue
5}
6let c: Color = Color.Green;
默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。或者,全部都采用手动赋值
1// 1、手动的指定成员的数值
2enum Color {Red = 1, Green, Blue}
3let c: Color = Color.Green;
4
5// 2、全部都采用手动赋值
6enum Color {Red = 1, Green = 2, Blue = 4}
7let c: Color = Color.Green;
4)object:表示非原始类型的值,例如对象、数组等。例如:
1let person: { name: string; age: number } = { name: "John", age: 30 };
特殊类型
1)any:表示任意类型,允许任何类型的值。通常用于处理动态内容或逐步迁移到 TypeScript 的项目。例如:
1let anything: any = "hello";
2anything = 10;
2)unknown:表示未知类型,与 any 类似,但更安全,必须在使用之前进行类型检查。例如:
1let notSure: unknown = 4;
2if (typeof notSure === "number") {
3 let sure: number = notSure;
4}
3)never:表示不会发生的值,通常用于标识函数从不会返回(如抛出异常)或永远不会有结果的情况。例如:
1function error(message: string): never {
2 throw new Error(message);
3}
4)void:表示没有返回值的函数。例如:
1function warnUser(): void {
2 console.log("This is a warning message");
3}
某种程度上来说,void 类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
扩展知识
除了上述常用类型,TypeScript 还支持一些高级类型和类型操作,比如:
1)联合类型(<font style="color:rgb(31, 35, 40);">|</font>
)和交叉类型(<font style="color:rgb(31, 35, 40);">&</font>
): 例如:
1let id: string | number;
2let person: Person & Serializable;
2)type 类型别名:用于为类型创建别名。 例如:
1type Point = { x: number; y: number; };
3)interface 接口:用于定义对象的类型。 例如:
1interface Person {
2 name: string;
3 age: number;
4}
5let john: Person = { name: "John", age: 30 };
什么是 TypeScript 的对象类型?怎么定义对象类型?
什么是 TypeScript 的对象类型?
在 TypeScript 中,对象类型用于描述非原始类型的值,比如具有特定结构的对象、数组和函数等。
如何定义对象类型?
我们可以通过 3 种主要方式来定义对象类型:匿名、类型别名、接口。
1)匿名对象。 可以直接用类 JavaScript 的语法定义对象属性,示例如下:
1function greet(person: { name: string; age: number }) {
2 return "Hello " + person.name;
3}
该函数接受包含属性 name(必须是 string)和 age(必须是 number)的对象。
2)类型别名。通过 <font style="color:rgb(31, 35, 40);">type</font>
关键字来创建,它为一个特定的对象类型创建了一个新名称。示例如下:
1type Person = {
2 name: string;
3 age: number;
4};
5
6function greet(person: Person) {
7 return "Hello " + person.name;
8}
类型别名适用于复杂的类型组合,如联合类型、交叉类型或条件类型。
3)接口。通过 <font style="color:rgb(31, 35, 40);">interface</font>
关键字定义,示例如下:
1interface Person {
2 name: string;
3 age: number;
4}
5
6function greet(person: Person) {
7 return "Hello " + person.name;
8}
接口与类型别名类似,但接口可以扩展(继承)其他接口:
1interface Employee extends Person {
2 employeeId: number;
3}
4
5let jane: Employee = {
6 name: "Jane",
7 age: 25,
8 employeeId: 1234
9};
接口还可以用于描述函数类型,示例如下:
1interface SearchFunc {
2 (source: string, subString: string): boolean;
3}
4
5let mySearch: SearchFunc;
6mySearch = function(source: string, subString: string) {
7 return source.search(subString) !== -1;
8};
扩展知识
TypeScript 的对象类型有很多实用的特性,下面分别讲解。
属性修饰符
对象类型中的每个属性都可以指定一些内容:类型、属性是否可选、属性是否可以写入。
1、可选属性
在对象类型中,我们可以使用 <font style="color:rgb(31, 35, 40);">?</font>
来标识可选属性:
1interface Person {
2 name: string;
3 age?: number; // age 是可选的
4}
5
6let john: Person = {
7 name: "John"
8};
2、只读属性
通过 <font style="color:rgb(31, 35, 40);">readonly</font>
关键字,可以定义只读属性,防止它们在对象创建后被修改:
1interface Point {
2 readonly x: number;
3 readonly y: number;
4}
5
6let p1: Point = { x: 10, y: 20 };
7// p1.x = 5; // 错误,x 是只读属性
3、索引签名
有时你无法提前知道对象属性的所有名称(key),但你可以明确 key 的类型,就可以使用索引签名。 索引签名允许对象具有未知数量的属性,比如:
1interface StringArray {
2 [index: number]: string;
3}
4
5let myArray: StringArray = ["Bob", "Fred"];
6let first: string = myArray[0]; // Bob
类型扩展
1、继承
接口是支持继承的,便于我们扩展对象类型,而且支持多继承,示例代码如下:
1interface Colorful {
2 color: string;
3}
4
5interface Circle {
6 radius: number;
7}
8
9interface ColorfulCircle extends Colorful, Circle {}
10
11const cc: ColorfulCircle = {
12 color: "red",
13 radius: 42,
14};
2、交叉类型
除了通过继承实现对象扩展外,TypeScript 还提供了交叉类型,用于组合现有的对象类型。 交叉类型是使用 <font style="color:rgb(31, 35, 40);">&</font>
运算符定义的,示例代码如下:
1interface Colorful {
2 color: string;
3}
4interface Circle {
5 radius: number;
6}
7
8type ColorfulCircle = Colorful & Circle;
上述代码将 Colorful 和 Circle 相交,生成了一个包含 Colorful 和 Circle 的所有成员的新类型。
泛型对象类型
TypeScript 还支持泛型对象类型,通过泛型,可以编写能够适用于多种类型的函数、类和接口,而无需在编写代码时指定具体的类型,能够使代码更具通用性和复用性。 常见的使用场景包括泛型接口、泛型类、泛型函数、泛型约束等,示例如下:
1、泛型接口
泛型接口允许我们定义可以适用于多种类型的接口。例如,定义一个可以操作不同类型数据的容器接口:
1interface Container<T> {
2 value: T;
3}
4
5let stringContainer: Container<string> = { value: "Hello, TypeScript" };
6let numberContainer: Container<number> = { value: 42 };
Array
2、泛型类
泛型类与泛型接口类似,允许定义可以操作多种类型数据的类。例如,定义一个泛型栈类:
1class Stack<T> {
2 private items: T[] = [];
3
4 push(item: T): void {
5 this.items.push(item);
6 }
7
8 pop(): T | undefined {
9 return this.items.pop();
10 }
11}
12
13let stringStack = new Stack<string>();
14stringStack.push("Hello");
15console.log(stringStack.pop()); // "Hello"
16
17let numberStack = new Stack<number>();
18numberStack.push(42);
19console.log(numberStack.pop()); // 42
3、泛型函数
可以灵活地定义函数的参数和返回值类型,例如,一个返回输入参数的函数:
1function identity<T>(arg: T): T {
2 return arg;
3}
4
5let output1 = identity<string>("myString");
6let output2 = identity<number>(100);
4、泛型约束
有时我们希望泛型类型满足某些条件,这时候可以使用泛型约束。例如,定义一个只能操作具有 length 属性的泛型函数:
1interface Lengthwise {
2 length: number;
3}
4
5function logLength<T extends Lengthwise>(arg: T): T {
6 console.log(arg.length);
7 return arg;
8}
9
10logLength("Hello"); // 输出: 5
11logLength([1, 2, 3]); // 输出: 3
12// logLength(42); // 错误: number 没有 length 属性
上述代码使用 <font style="color:rgb(31, 35, 40);">extends</font>
关键字约束 T 必须满足 Lengthwise 接口,即必须具有 length 属性。
type和 interface
回答重点
TypeScript 的类型别名和接口都有助于定义复杂类型,但它们存在一些关键区别: 1)用途不同 2)扩展方式不同 3)合并机制不同
1)用途不同
类型别名(Type Aliases)可以用于定义原始类型、联合类型、元组以及复杂对象等各种类型。接口(Interfaces)则主要用于定义对象类型。
2)扩展方式不同
接口可以通过<font style="color:rgb(31, 35, 40);">extends</font>
关键字进行扩展,而类型别名则需要使用交叉类型 <font style="color:rgb(31, 35, 40);">&</font>
来进行组合。
3)合并机制不同
接口支持声明合并,即可以多次声明同一个接口名称,它们会自动合并。而类型别名不支持这一点,重复声明同名的别名会导致编译错误。
扩展知识
在实际应用中,选择类型别名还是接口取决于具体需求和个人习惯。下面我详细说明几种情况和使用方式:
1)定义基本类型和联合类型
类型别名可以定义简单的类型别名以及联合类型,这是接口所不能直接实现的。
1type Primitive = string | number;
2type PersonTuple = [string, number];
2)接口的声明合并
接口的声明合并特性在大型项目中非常有用,特别是当你使用第三方库且需要为其添加额外的属性时。
1interface User {
2 name: string;
3}
4
5// Later, in another location:
6interface User {
7 age: number;
8}
9
10// Resulting type: { name: string; age: number; }
3)继承和交叉类型
接口通过 <font style="color:rgb(31, 35, 40);">extends</font>
进行继承,而类型别名使用交叉类型 <font style="color:rgb(31, 35, 40);">&</font>
来实现组合。
1interface Animal {
2 name: string;
3}
4
5interface Dog extends Animal {
6 breed: string;
7}
8
9type Animal2 = { name: string };
10type Dog2 = Animal2 & { breed: string };
4)函数与类的类型
类型别名可以用于定义函数类型和类类型,接口也可以做到这一点,但类型别名更简洁。
1type Log = (message: string) => void;
2
3interface Log2 {
4 (message: string): void;
5}
TypeScript 的类有哪些成员可见性?
TypeScript 的类成员有三个主要的可见性修饰符:public、private 和 protected。
1)public: 默认的修饰符,表示类成员可以在任何地方访问,没有限制。
2)private: 表示类成员只能在声明它的类内部访问,不能在类的实例以及子类中访问。
3)protected: 表示类成员可以在声明它的类及其子类中访问,但不能在类的实例中访问。
git
1下面是常用 的Git 命令清单。几个专用名词的译名如下:
2Workspace:工作区
3Index / Stage:暂存区
4Repository:仓库区(或本地仓库)
5Remote:远程仓库
6复制代码
7一、新建代码库
8# 在当前目录新建一个Git代码库
9$ git init
10
11# 新建一个目录,将其初始化为Git代码库
12$ git init [project-name]
13
14# 下载一个项目和它的整个代码历史
15$ git clone [url]
16复制代码
17二、配置
18Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。
19# 显示当前的Git配置
20$ git config --list
21
22# 编辑Git配置文件
23$ git config -e [--global]
24
25# 设置提交代码时的用户信息
26$ git config [--global] user.name "[name]"
27$ git config [--global] user.email "[email address]"
28复制代码
29三、增加/删除文件
30# 添加指定文件到暂存区
31$ git add [file1] [file2] ...
32
33# 添加指定目录到暂存区,包括子目录
34$ git add [dir]
35
36# 添加当前目录的所有文件到暂存区
37$ git add .
38
39# 添加每个变化前,都会要求确认
40# 对于同一个文件的多处变化,可以实现分次提交
41$ git add -p
42
43# 删除工作区文件,并且将这次删除放入暂存区
44$ git rm [file1] [file2] ...
45
46# 停止追踪指定文件,但该文件会保留在工作区
47$ git rm --cached [file]
48
49# 改名文件,并且将这个改名放入暂存区
50$ git mv [file-original] [file-renamed]
51复制代码
52四、代码提交
53# 提交暂存区到仓库区
54$ git commit -m [message]
55
56# 提交暂存区的指定文件到仓库区
57$ git commit [file1] [file2] ... -m [message]
58
59# 提交工作区自上次commit之后的变化,直接到仓库区
60$ git commit -a
61
62# 提交时显示所有diff信息
63$ git commit -v
64
65# 使用一次新的commit,替代上一次提交
66# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
67$ git commit --amend -m [message]
68
69# 重做上一次commit,并包括指定文件的新变化
70$ git commit --amend [file1] [file2] ...
71复制代码
72五、分支
73# 列出所有本地分支
74$ git branch
75
76# 列出所有远程分支
77$ git branch -r
78
79# 列出所有本地分支和远程分支
80$ git branch -a
81
82# 新建一个分支,但依然停留在当前分支
83$ git branch [branch-name]
84
85# 新建一个分支,并切换到该分支
86$ git checkout -b [branch]
87
88# 新建一个分支,指向指定commit
89$ git branch [branch] [commit]
90
91# 新建一个分支,与指定的远程分支建立追踪关系
92$ git branch --track [branch] [remote-branch]
93
94# 切换到指定分支,并更新工作区
95$ git checkout [branch-name]
96
97# 切换到上一个分支
98$ git checkout -
99
100# 建立追踪关系,在现有分支与指定的远程分支之间
101$ git branch --set-upstream [branch] [remote-branch]
102
103# 合并指定分支到当前分支
104$ git merge [branch]
105
106# 选择一个commit,合并进当前分支
107$ git cherry-pick [commit]
108
109# 删除分支
110$ git branch -d [branch-name]
111
112# 删除远程分支
113$ git push origin --delete [branch-name]
114$ git branch -dr [remote/branch]
115复制代码
116六、标签
117# 列出所有tag
118$ git tag
119
120# 新建一个tag在当前commit
121$ git tag [tag]
122
123# 新建一个tag在指定commit
124$ git tag [tag] [commit]
125
126# 删除本地tag
127$ git tag -d [tag]
128
129# 删除远程tag
130$ git push origin :refs/tags/[tagName]
131
132# 查看tag信息
133$ git show [tag]
134
135# 提交指定tag
136$ git push [remote] [tag]
137
138# 提交所有tag
139$ git push [remote] --tags
140
141# 新建一个分支,指向某个tag
142$ git checkout -b [branch] [tag]
143复制代码
144七、查看信息
145# 显示有变更的文件
146$ git status
147
148# 显示当前分支的版本历史
149$ git log
150
151# 显示commit历史,以及每次commit发生变更的文件
152$ git log --stat
153
154# 搜索提交历史,根据关键词
155$ git log -S [keyword]
156
157# 显示某个commit之后的所有变动,每个commit占据一行
158$ git log [tag] HEAD --pretty=format:%s
159
160# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
161$ git log [tag] HEAD --grep feature
162
163# 显示某个文件的版本历史,包括文件改名
164$ git log --follow [file]
165$ git whatchanged [file]
166
167# 显示指定文件相关的每一次diff
168$ git log -p [file]
169
170# 显示过去5次提交
171$ git log -5 --pretty --oneline
172
173# 显示所有提交过的用户,按提交次数排序
174$ git shortlog -sn
175
176# 显示指定文件是什么人在什么时间修改过
177$ git blame [file]
178
179# 显示暂存区和工作区的差异
180$ git diff
181
182# 显示暂存区和上一个commit的差异
183$ git diff --cached [file]
184
185# 显示工作区与当前分支最新commit之间的差异
186$ git diff HEAD
187
188# 显示两次提交之间的差异
189$ git diff [first-branch]...[second-branch]
190
191# 显示今天你写了多少行代码
192$ git diff --shortstat "@{0 day ago}"
193
194# 显示某次提交的元数据和内容变化
195$ git show [commit]
196
197# 显示某次提交发生变化的文件
198$ git show --name-only [commit]
199
200# 显示某次提交时,某个文件的内容
201$ git show [commit]:[filename]
202
203# 显示当前分支的最近几次提交
204$ git reflog
205复制代码
206八、远程同步
207# 下载远程仓库的所有变动
208$ git fetch [remote]
209
210# 显示所有远程仓库
211$ git remote -v
212
213# 显示某个远程仓库的信息
214$ git remote show [remote]
215
216# 增加一个新的远程仓库,并命名
217$ git remote add [shortname] [url]
218
219# 取回远程仓库的变化,并与本地分支合并
220$ git pull [remote] [branch]
221
222# 上传本地指定分支到远程仓库
223$ git push [remote] [branch]
224
225# 强行推送当前分支到远程仓库,即使有冲突
226$ git push [remote] --force
227
228# 推送所有分支到远程仓库
229$ git push [remote] --all
230复制代码
231九、撤销
232# 恢复暂存区的指定文件到工作区
233$ git checkout [file]
234
235# 恢复某个commit的指定文件到暂存区和工作区
236$ git checkout [commit] [file]
237
238# 恢复暂存区的所有文件到工作区
239$ git checkout .
240
241# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
242$ git reset [file]
243
244# 重置暂存区与工作区,与上一次commit保持一致
245$ git reset --hard
246
247# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
248$ git reset [commit]
249
250# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
251$ git reset --hard [commit]
252
253# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
254$ git reset --keep [commit]
255
256# 新建一个commit,用来撤销指定commit
257# 后者的所有变化都将被前者抵消,并且应用到当前分支
258$ git revert [commit]
259
260# 暂时将未提交的变化移除,稍后再移入
261$ git stash
262$ git stash pop
263复制代码
264十、其他
265# 生成一个可供发布的压缩包
266$ git archive
267
268
H5 移动端适配常见方案有
1. 流式布局(百分比布局)
📌 原理:
- 通过
width: %
让元素随屏幕大小等比缩放。 - 适用于简单页面,如文章、图片展示等。
📌 优点:
✔ 兼容性好,支持所有浏览器。
✔ 简单易用,不依赖 JS 或额外插件。
📌 缺点:
❌ 复杂布局控制不灵活,容易导致元素错乱。
❌ 文字大小不会随屏幕缩放,需要额外处理 font-size
。
📌 示例代码:
1.container {
2 width: 90%;
3 margin: 0 auto;
4}
2. 弹性布局(Flexbox 适配)
📌 原理:
display: flex
让子元素按比例分配空间,适用于横向、纵向自适应布局。
📌 优点:
✔ 适用于现代移动端,布局灵活。
✔ 适合等比例子元素分布,如 flex: 1
自动适应宽度。
📌 缺点:
❌ flex-shrink
可能导致文本/按钮被挤压,需要 min-width
限制。
📌 示例代码:
1.container {
2 display: flex;
3 justify-content: space-between;
4}
5.item {
6 flex: 1;
7}
3. REM 适配(rem + 动态 font-size)
📌 原理:
- 通过
rem
作为单位,动态设置html { font-size: ... }
来适配不同屏幕。 - 适用于设计稿宽度固定(如 750px)的项目。
📌 实现方式:
1️⃣ **JS 计算 ****font-size**
(不推荐)
2️⃣ **PostCSS 自动转换 ****px -> rem**
(推荐)
📌 优点:
✔ 让 UI 设计稿等比例缩放,不影响布局。
✔ 结合 postcss-pxtorem
自动转换 px,适配不同屏幕。
📌 缺点:
❌ rem
计算不直观,可能需要调试 font-size
。
📌 示例:JS 计算 **rem**
(Flexible.js)
1(function() {
2 var baseSize = 37.5; // 750px 设计稿
3 function setRem() {
4 var scale = document.documentElement.clientWidth / 375;
5 document.documentElement.style.fontSize = (baseSize * scale) + 'px';
6 }
7 setRem();
8 window.onresize = setRem;
9})();
📌 示例:PostCSS 自动转换 px -> rem
1module.exports = {
2 plugins: {
3 'postcss-pxtorem': {
4 rootValue: 37.5,
5 propList: ['*']
6 }
7 }
8}
4. VW/VH 适配
📌 原理:
- 直接使用
vw
(视口宽度)、vh
(视口高度)让元素按屏幕大小缩放。 1vw = 屏幕宽度的 1%
,1vh = 屏幕高度的 1%
。
📌 优点:
✔ vw
适用于全屏 UI(如背景图、轮播图)。
✔ vh
可用于全屏页面(如 height: 100vh;
)。
📌 缺点:
❌ 兼容性略逊于 rem
,特别是 iOS 12 以下 vh
表现不稳定。
📌 示例代码:
1.container {
2 width: 100vw;
3 height: 50vh;
4}
5. 媒体查询(Media Query)
📌 原理:
- 通过
@media
定义不同屏幕尺寸的样式。 - 适用于多端适配(PC、iPad、手机)。
📌 优点:
✔ 适用于复杂布局,精准控制不同设备样式。
✔ 适合响应式开发(如 PC 适配手机)。
📌 缺点:
❌ 代码量大,可能需要维护多个样式文件。
📌 示例代码:
1@media screen and (max-width: 768px) {
2 body {
3 font-size: 14px;
4 }
5}
6. viewport + DPR 适配
📌 原理:
viewport
控制页面缩放,避免1px
变粗问题。- 结合
window.devicePixelRatio
处理高清屏适配。
📌 优点:
✔ 适用于高清屏(如 iPhone X、安卓高分屏)。
✔ 可结合 1px 适配方案
,避免 border
变粗。
📌 缺点:
❌ 需要 meta viewport
配置,部分 Android 兼容性差。
📌 示例代码:
1<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no
移动端 1px 变粗问题怎么解决?
原因:
- 高清屏(如 iPhoneX)
devicePixelRatio (DPR) > 1
,导致 1px 实际显示变粗。
📌 解决方案:
1.border-1px {
2 position: relative;
3}
4.border-1px::after {
5 content: "";
6 position: absolute;
7 width: 100%;
8 height: 1px;
9 background-color: #ddd;
10 transform: scaleY(0.5); /* 适配高 DPR 设备 */
11}
H5 页面如何监听键盘弹起和收起?
1window.addEventListener(‘resize’, () => {
2
3if (document.activeElement.tagName === ‘INPUT’) {
4
5console.log('键盘弹起');
6
7} else {
8
9console.log('键盘收起');
10
11}
12
13});
📌 问题点:
iOS 可能无法触发 resize,可监听 focus/blur:
1document.querySelector(‘input’).addEventListener(‘blur’, function() {
2
3console.log(‘键盘收起’);
4
5});
H5 如何实现 touch 事件?
📌 答:
touchstart
:手指触摸屏幕touchmove
:手指滑动touchend
:手指离开屏幕touchcancel
:中断触摸
📌 示例代码:
1document.addEventListener('touchstart', function(event) {
2 console.log('触摸开始', event.touches[0].clientX);
3});
移动端点击 300ms 延迟怎么解决?
📌 原因:
- 旧版 iOS 需要 双击放大,所以首次点击后会等待 300ms 判断用户是否 双击。
📌 解决方案:
- **使用 **
**FastClick.js**
(适用于旧版浏览器)
1import FastClick from 'fastclick';
2FastClick.attach(document.body);
- CSS 方案(适用于现代浏览器)
1html {
2 touch-action: manipulation;
3}
**pointer-events: none**
** 临时禁用点击**
H5 如何监听横竖屏切换?
📌 答:
1window.addEventListener('orientationchange', function() {
2 console.log(window.orientation === 0 ? '竖屏' : '横屏');
3});
📌 或者:
1window.matchMedia("(orientation: landscape)").addEventListener("change", (e) => {
2 console.log(e.matches ? "横屏" : "竖屏");
3});
iOS 点击 **input**
,键盘弹起后 **fixed**
失效
📌 原因:
- iOS 键盘弹起时,
fixed
元素会跟随滚动,导致位置错乱。
📌 解决方案: 1️⃣ 键盘收起时恢复**scroll**
位置
1window.addEventListener('focusout', () => {
2 window.scrollTo(0, 0);
3});
2️⃣ **避免 **position: fixed**
,改用 ****absolute + bottom: 0**
**input**
在 iOS 中 **blur()**
失效
📌 原因:
- iOS 默认不会主动关闭键盘,
blur()
无法触发收起。
📌 解决方案:
1document.activeElement.blur(); // 强制失焦
iOS 微信内置浏览器 **position: fixed**
失效
📌 原因:
- 微信
WebView
的 滚动机制 导致fixed
定位错误。
📌 解决方案:
1body {
2 position: fixed;
3 width: 100%;
4}
安卓机型 **touchmove**
卡顿
📌 原因:
- 安卓默认
touchmove
可能触发浏览器 回弹/缩放 导致卡顿。
📌 解决方案:
1document.addEventListener('touchmove', function(event) {
2 event.preventDefault(); // 禁止默认滚动
3}, { passive: false });
iOS 微信浏览器 **overflow: scroll**
滚动卡顿
📌 原因:
- iOS 微信浏览器
overflow: scroll
默认 不会有弹性滚动。
📌 解决方案:
1.container {
2 -webkit-overflow-scrolling: touch; /* 启用惯性滚动 */
3}
**background: fixed**
在 iOS 下失效
📌 原因:
background-attachment: fixed
在 iOS 上可能会失效。
📌 解决方案:- 方法 1:使用
position: fixed
代替background: fixed
。 - 方法 2:用
div
伪元素代替background
:
1.bg {
2 position: fixed;
3 width: 100vw;
4 height: 100vh;
5 background: url('xxx.jpg') no-repeat center / cover;
6}
iOS 微信 X5 内核 **<video>**
自动播放失败
📌 原因:
- iOS 默认禁止
<video>
在未用户交互情况下自动播放。
📌 解决方案:
1<video autoplay playsinline muted>
2 <source src="video.mp4" type="video/mp4">
3</video>
playsinline
:防止全屏播放muted
:部分 iOS 设备 静音时可自动播放
JS笔试题
公共前缀
1function longestCommonPrefix(strs) {
2 if (!strs.length) return "";
3 let prefix = strs[0];
4 for (let str of strs) {
5 while (!str.startsWith(prefix)) {
6 prefix = prefix.slice(0, -1);
7 if (!prefix) return "";
8 }
9 }
10 return prefix;
11}
12console.log(longestCommonPrefix(["flower", "flow", "flight"])); // "fl"
13console.log(longestCommonPrefix(["dog", "racecar", "car"])); // ""
数组排序
1function bubbleSort(arr) {
2 let len = arr.length;
3 for (let i = 0; i < len - 1; i++) {
4 for (let j = 0; j < len - i - 1; j++) {
5 if (arr[j] > arr[j + 1]) {
6 [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换
7 }
8 }
9 }
10 return arr;
11}
12console.log(bubbleSort([5, 3, 8, 4, 2])); // [2, 3, 4, 5, 8]
1let arr = [5, 2, 9, 1, 5, 6, 2, 9];
2
3// 先排序,再使用 Set 去重
4let sortedUniqueArr = [...new Set(arr.sort((a, b) => a - b))];
5
6console.log(sortedUniqueArr); // [1, 2, 5, 6, 9]
统计数组中某些字段的值出现的次数
1function countOccurrences(arr, keys) {
2 let result = {};
3
4 keys.forEach(key => {
5 result[key] = arr.reduce((acc, item) => {
6 acc[item[key]] = (acc[item[key]] || 0) + 1;
7 return acc;
8 }, {});
9 });
10
11 return result;
12}
13
14// 示例数据
15let people = [
16 { name: "Alice", age: 25 },
17 { name: "Bob", age: 20 },
18 { name: "Alice", age: 30 },
19 { name: "Charlie", age: 25 },
20 { name: "Bob", age: 25 }
21];
22
23// 调用方法
24let counts = countOccurrences(people, ["name", "age"]);
25
26console.log(counts.name); // { Alice: 2, Bob: 2, Charlie: 1 }
27console.log(counts.age); // { 25: 3, 20: 1, 30: 1 }
防抖
1function debounce(fn, delay = 300) {
2 let timer = null;
3 return function (...args) {
4 if (timer) clearTimeout(timer); // 清除之前的定时器
5 timer = setTimeout(() => {
6 fn.apply(this, args);
7 }, delay);
8 };
9}
10
11// 示例:防抖输入框事件
12const inputHandler = debounce((e) => {
13 console.log('输入内容:', e.target.value);
14}, 500);
15
16document.getElementById('search').addEventListener('input', inputHandler);
节流
1function throttle(fn, delay = 300) {
2 let lastTime = 0;
3 return function (...args) {
4 let now = Date.now();
5 if (now - lastTime >= delay) {
6 fn.apply(this, args);
7 lastTime = now;
8 }
9 };
10}
11
12// 示例:节流滚动事件
13const scrollHandler = throttle(() => {
14 console.log('滚动中...', new Date().toLocaleTimeString());
15}, 1000);
16
17window.addEventListener('scroll', scrollHandler);
jQuery
1. jQuery 是什么?它的核心特点是什么?
jQuery 是一个 轻量级、跨浏览器 的 JavaScript 库,简化了 DOM 操作、事件处理、AJAX 请求和动画效果。
核心特点包括:
- 链式调用(Chaining):可以连续操作多个方法,如
$('#id').addClass('active').fadeIn();
- 简化 DOM 操作:如
$('#id').text('Hello')
- 内置事件处理:如
.click()
、.on()
- 兼容性好:自动处理浏览器兼容性问题
- 强大的 AJAX 支持:如
$.ajax()
- 插件机制:方便扩展功能
2. **$(document).ready()**
的作用是什么?
它用于确保 DOM 加载完成后再执行 jQuery 代码,避免操作未加载的元素。
等价于:
1$(function() {
2 // jQuery 代码
3});
或者:
1$(document).ready(function() {
2 // jQuery 代码
3});
**对比 ****window.onload**
:
$(document).ready()
只等待 DOM 加载完成,不等图片等资源加载。window.onload
需要等页面所有资源(包括图片、CSS 等)加载完。
3. jQuery 选择器有哪些?
jQuery 提供强大的 CSS 选择器,常见的包括:
- ID 选择器:
$('#id')
- 类选择器:
$('.class')
- 标签选择器:
$('div')
- 层级选择器:
1$('#parent .child') // 选择 parent 内的所有 child
2$('ul > li') // 只选直接子元素
- 属性选择器:
1$('input[type="text"]') // 选中所有文本框
2$('a[href^="https"]') // 选中所有 https 开头的链接
- 伪类选择器:
1$('li:first') // 选中第一个 li
2$(':hidden') // 选中隐藏元素
4. jQuery 中 **.on()**
和 **.bind()**
、**.live()**
** 的区别?**
**.bind(event, handler)**
:绑定事件,但不能作用于动态添加的元素。**.live(event, handler)**
(已废弃):绑定事件,可以作用于动态元素,但性能差。**.delegate(selector, event, handler)**
:绑定事件到父元素,适用于动态元素。**.on(event, selector, handler)**
(推荐):
1$(document).on('click', '.btn', function() {
2 alert('按钮被点击');
3});
优势:适用于动态元素,比 .live()
性能更好。
5. jQuery 操作 DOM 的方法有哪些?
- 修改内容
1$('#id').text('新内容'); // 修改文本
2$('#id').html('<b>加粗</b>'); // 修改 HTML
3$('#id').val('123'); // 修改输入框值
- 修改属性
1$('img').attr('src', 'new.jpg'); // 修改属性
2$('#checkbox').prop('checked', true); // 修改表单属性
- 修改样式
1$('#box').css('color', 'red'); // 修改样式
2$('#box').addClass('active'); // 添加类
3$('#box').removeClass('hidden'); // 移除类
- 插入元素
1$('#parent').append('<div>后插入</div>'); // 末尾插入
2$('#parent').prepend('<div>前插入</div>'); // 开头插入
3$('#child').before('<div>前面插入</div>'); // 前插
4$('#child').after('<div>后面插入</div>'); // 后插
- 删除元素
1$('#id').remove(); // 删除自身
2$('#id').empty(); // 清空子元素
6. jQuery 如何发 AJAX 请求?
使用 **$.ajax()**
或 **$.get()**
** / ****$.post()**
发送请求:
1$.ajax({
2 url: '/api/data',
3 type: 'GET',
4 dataType: 'json',
5 success: function(response) {
6 console.log('数据:', response);
7 },
8 error: function(xhr) {
9 console.error('请求失败', xhr);
10 }
11});
简化版:
1$.get('/api/data', function(response) {
2 console.log(response);
3});
7. jQuery 如何优化性能?
- 缓存选择器:
1var $box = $('#box'); // 缓存 jQuery 对象
2$box.addClass('active');
- 减少 DOM 操作(合并操作):
1var newItem = $('<li>新项</li>');
2$('#list').append(newItem);
- 事件委托(
**on**
):
1$(document).on('click', '.dynamic-item', function() {
2 console.log('动态元素点击');
3});
- 使用
**.detach()**
处理 DOM 批量操作:
1var $list = $('#list').detach();
2$list.append('<li>新项</li>');
3$('body').append($list);
- **避免使用
**:visible**
和 ****:hidden**
,用.css('display')
或.is(':visible')
。
8. jQuery 如何处理动画?
- 基本动画:
1$('#box').fadeIn(); // 淡入
2$('#box').fadeOut(); // 淡出
3$('#box').slideUp(); // 收起
4$('#box').slideDown(); // 展开
- 自定义动画:
1$('#box').animate({ width: '300px', opacity: 0.5 }, 500);
9. jQuery 如何防止事件冒泡和默认行为?
1$('#link').click(function(event) {
2 event.preventDefault(); // 阻止默认行为
3 event.stopPropagation(); // 阻止事件冒泡
4});
10. jQuery 和 Vue/React 有什么区别?
- jQuery:直接操作 DOM,适合 简单交互 或 老项目。
- Vue/React:基于 数据驱动,组件化管理,适用于 复杂应用,性能更好。
- 关键点:
- jQuery 修改 DOM,Vue/React 修改数据,DOM 自动更新。
- jQuery 没有双向绑定,Vue/React 数据和视图同步。
浏览器兼容性解决方案
1)CSS 解决方案
- 使用
**Autoprefixer**
自动补全前缀
1display: flex;
2-webkit-display: flex;
3-moz-display: flex;
4-ms-display: flex;
- 使用
**normalize.css**
统一默认样式
1<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
2)JavaScript 解决方案
- 使用 Babel 转译
1npm install @babel/preset-env --save-dev
配置 .babelrc
:
1{
2 "presets": ["@babel/preset-env"]
3}
- **使用 **
**Polyfill.js**
1<script src="https://polyfill.io/v3/polyfill.min.js"></script>
3)针对 IE 低版本的解决方案
- IE 专属样式
1@media all and (-ms-high-contrast: none) {
2 /* 仅在 IE 显示 */
3}
- 条件注释
1<!--[if lt IE 9]>
2<script src="html5shiv.js"></script>
3<![endif]-->
4. 最佳实践
- 尽量使用标准代码,减少 hack
- **使用
**feature detection**
而不是 ****browser detection**
1if ('fetch' in window) {
2 fetchData();
3} else {
4 useAjax();
5}
- **使用
**Graceful Degradation(优雅降级)**
和 ****Progressive Enhancement(渐进增强)**
- **尽量避免
**innerHTML**
操作 DOM,使用 ****createElement**
- 利用
**PostCSS**
处理 CSS 兼容问题
总结
浏览器兼容问题主要体现在 CSS、JS、HTML5、AJAX 方面,核心解决方案包括:
- CSS:
**Autoprefixer**
、**normalize.css**
- JS:
**Babel**
、**Polyfill**
、事件兼容 - HTML5:条件注释、检测 API
- AJAX:
**fetch()**
** polyfill、XDomainRequest 兼容**
h5开发移动端遇到的bug
JavaScript 在 ios 时间 显示 NaN
造成的原因:
- 服务端传来的时间格式如果是 2017-02-16 20:41:10 这种格式的,需要把横杆转为斜杠,例如:
解决方法
- newDate(“2017-02-16 20:41:10”.replace(/-/g,“/”)).getFullYear()
H5在ios上把某些数字变色
ios input输入时白屏
IOS和安卓一些标签使用
- 不能用 overflow:auto 不然会隐藏 按钮
- 不能用 不然不能正常显示
移动端上传问题
ios手机可以正常进行上传文件,安卓手机不能正常上传问题,所以移动端开发,涉及到上传文件的地方最好用对应环境提供的第三方api,比如微信公众号,小程序,钉钉开放,H5嵌套app混合开发等等,都用第三方封装好的上传接口来进行上传文件
iOS 滑动不流畅
表现
上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。
产生原因
为什么 iOS 的 webview 中 滑动不流畅,它是如何定义的?
最终我在 safari 文档里面寻找到了答案(文档链接在参考资料项)。
-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 / -webkit-overflow-scrolling: auto; / 当手指从触摸屏上移开,滚动会立即停止 */
原来在 iOS 5.0 以及之后的版本,滑动有定义有两个值 auto 和 touch,默认值为 auto。
解决方案
1.在滚动容器上增加滚动 touch 方法
将-webkit-overflow-scrolling 值设置为 touch
设置滚动条隐藏: .container ::-webkit-scrollbar {display: none;}
body { overflow-y: hidden; } .wrapper { overflow-y: auto; }
可能会导致使用position:fixed; 固定定位的元素,随着页面一起滚动
2.设置 overflow
设置外部 overflow 为 hidden,设置内容元素 overflow 为 auto。内部元素超出 body 即产生滚动,超出的部分 body 隐藏。
两者结合使用更佳!
iOS 上拉边界下拉出现白色空白
- touchstart :手指放在一个DOM元素上。 2. touchmove :手指拖曳一个DOM元素。 3. touchend :手指从一个DOM元素上移开。 复制代码复制代码
表现
手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。
产生原因
在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。
解决方案
1. 监听事件禁止滑动
2. 滚动妥协填充空白,装饰成其他功能
在很多时候,我们可以不去解决这个问题,换一直思路。根据场景,我们可以将下拉作为一个功能性的操作。
比如: 下拉后刷新页面
页面放大或缩小不确定性行为
表现
双击或者双指张开手指页面元素,页面会放大或缩小。
产生原因
HTML 本身会产生放大或缩小的行为,比如在 PC 浏览器上,可以自由控制页面的放大缩小。但是在移动端,我们是不需要这个行为的。所以,我们需要禁止该不确定性行为,来提升用户体验。
原理与解决方案
HTML meta 元标签标准中有个 中 viewport 属性,用来控制页面的缩放,一般用于移动端。如下图 MDN 中介绍
click 点击事件延时与穿透
表现
监听元素 click 事件,点击元素触发时间延迟约 300ms。
点击蒙层,蒙层消失后,下层元素点击触发。
产生原因
为什么会产生 click 延时?
iOS 中的 safari,为了实现双击缩放操作,在单击 300ms 之后,如果未进行第二次点击,则执行 click 单击操作。也就是说来判断用户行为是否为双击产生的。但是,在 App 中,无论是否需要双击缩放这种行为,click 单击都会产生 300ms 延迟。
为什么会产生 click 点击穿透?
双层元素叠加时,在上层元素上绑定 touch 事件,下层元素绑定 click 事件。由于 click 发生在 touch 之后,点击上层元素,元素消失,下层元素会触发 click 事件,由此产生了点击穿透的效果。
原理与解决方案
主要目的就是,在使用 touchstart 合成 click 事件时,保证其不在滚动的父元素之下。
后台遇到的bug
切换页面时页面空白
虽然vue3支持多根节点但是 解决方法是 在最外层包裹一个div
动态渲染路由刷新页面丢失问题
scoped样式不生效
最外层元素选择器>>>想要生效的元素
vue3引用elementplus组件没有样式只有文字不生效的问题
main.js 语法错误
element plus之 table数据更新而视图不更新
Vue3+Element-Plus点击列表中的图片预览时,图片被表格覆盖问题
uniapp 遇到的bug
Next.js SEO优化详细步骤
从0到1的Next.js SEO优化详细步骤
为了从头到尾完成 Next.js 项目的 SEO 优化,我们将逐步解释每个步骤,并确保覆盖从配置基础到高级优化的所有方面。
步骤 1:创建一个 Next.js 项目
首先,你需要创建一个 Next.js 项目。如果你还没有项目,按以下步骤创建。
1# 安装 Next.js 项目
2npx create-next-app@latest my-next-seo-project
3
4# 进入项目目录
5cd my-next-seo-project
6
7# 安装依赖
8npm install
步骤 2:安装必要的 SEO 优化依赖
Next.js 默认支持很多 SEO 优化功能,但有些额外的工具可以帮助我们更好地优化 SEO。例如,next-seo
是一个非常受欢迎的 Next.js SEO 插件,能够帮助你更方便地设置 SEO 元信息。
1# 安装 next-seo
2npm install next-seo
步骤 3:设置 _app.js
和 _document.js
在 Next.js 项目中,_app.js
和 _document.js
是全局配置文件,它们可以帮助我们为所有页面设置公共的 SEO 元信息。
创建 _app.js
在 pages/_app.js
中引入 next-seo
,并配置基础的 SEO 设置。
1import { DefaultSeo } from 'next-seo';
2import '../styles/globals.css';
3
4function MyApp({ Component, pageProps }) {
5 return (
6 <>
7 <DefaultSeo
8 titleTemplate="%s | 我的Next.js网站"
9 defaultTitle="我的Next.js网站"
10 description="这是一个使用Next.js构建的SEO优化网站"
11 openGraph={{
12 type: 'website',
13 locale: 'zh_CN',
14 url: 'https://www.my-next-seo-project.com',
15 site_name: 'My Next SEO Project',
16 }}
17 twitter={{
18 handle: '@mytwitter',
19 site: '@mysite',
20 cardType: 'summary_large_image',
21 }}
22 />
23 <Component {...pageProps} />
24 </>
25 );
26}
27
28export default MyApp;
创建 _document.js
_document.js
文件是 Next.js 渲染 HTML 结构的地方,你可以在这里注入一些全局的 <head>
元素。
1import Document, { Html, Head, Main, NextScript } from 'next/document';
2
3class MyDocument extends Document {
4 render() {
5 return (
6 <Html lang="zh-CN">
7 <Head>
8 <meta name="robots" content="index, follow" />
9 <meta name="author" content="作者姓名" />
10 <meta charSet="UTF-8" />
11 <link rel="icon" href="/favicon.ico" />
12 {/* 可以在这里添加更多全局的SEO meta信息 */}
13 </Head>
14 <body>
15 <Main />
16 <NextScript />
17 </body>
18 </Html>
19 );
20 }
21}
22
23export default MyDocument;
步骤 4:设置每个页面的 SEO 元数据
在 Next.js 中,你可以为每个页面设置不同的 SEO 元数据。我们将使用 next-seo
或 next/head
进行每个页面的优化。
使用 next-seo
优化页面 SEO
next-seo
插件可以简化 SEO 配置,特别是对于 Open Graph 和 Twitter 卡片等内容。
1import { NextSeo } from 'next-seo';
2
3const Page = () => (
4 <>
5 <NextSeo
6 title="首页"
7 description="这是我使用 Next.js 创建的网站"
8 openGraph={{
9 type: 'website',
10 locale: 'zh_CN',
11 url: 'https://www.my-next-seo-project.com',
12 title: '首页',
13 description: '这是我使用 Next.js 创建的网站',
14 images: [
15 {
16 url: 'https://www.example.com/images/og-image.jpg',
17 width: 800,
18 height: 600,
19 alt: '首页图片',
20 },
21 ],
22 site_name: '我的Next.js网站',
23 }}
24 />
25 <h1>欢迎来到我的网站</h1>
26 <p>这是一个简单的 Next.js SEO 优化示例。</p>
27 </>
28);
29
30export default Page;
使用 next/head
优化页面 SEO
如果不想使用 next-seo
插件,你可以直接使用 next/head
来手动设置 <head>
元信息。
1import Head from 'next/head';
2
3const Page = () => (
4 <>
5 <Head>
6 <title>首页</title>
7 <meta name="description" content="这是我使用 Next.js 创建的网站" />
8 <meta name="keywords" content="Next.js, SEO, 网站" />
9 <meta property="og:title" content="首页" />
10 <meta property="og:description" content="这是我使用 Next.js 创建的网站" />
11 <meta property="og:image" content="https://www.example.com/images/og-image.jpg" />
12 <meta property="og:url" content="https://www.my-next-seo-project.com" />
13 </Head>
14 <h1>欢迎来到我的网站</h1>
15 <p>这是一个简单的 Next.js SEO 优化示例。</p>
16 </>
17);
18
19export default Page;
步骤 5:启用服务器端渲染(SSR)和静态生成(SSG)
Next.js 支持服务器端渲染(SSR)和静态生成(SSG),它们是 SEO 优化的关键。根据页面内容的动态性,选择合适的渲染方法。
使用 getServerSideProps
启用 SSR
1const Page = ({ data }) => (
2 <div>
3 <h1>{data.title}</h1>
4 <p>{data.content}</p>
5 </div>
6);
7
8export async function getServerSideProps() {
9 const res = await fetch('https://api.example.com/data');
10 const data = await res.json();
11 return { props: { data } };
12}
13
14export default Page;
使用 getStaticProps
启用 SSG
1const BlogPage = ({ posts }) => (
2 <div>
3 <h1>博客</h1>
4 <ul>
5 {posts.map((post) => (
6 <li key={post.id}>{post.title}</li>
7 ))}
8 </ul>
9 </div>
10);
11
12export async function getStaticProps() {
13 const res = await fetch('https://api.example.com/posts');
14 const posts = await res.json();
15 return { props: { posts } };
16}
17
18export default BlogPage;
步骤 6:优化图片加载
Next.js 提供了 next/image
组件,它自动优化图片加载,支持懒加载、自动缩放等功能。
1import Image from 'next/image';
2
3const ProductPage = () => (
4 <div>
5 <h1>产品页面</h1>
6 <Image
7 src="/path/to/image.jpg"
8 alt="产品图片"
9 width={500}
10 height={500}
11 priority // 使图片优先加载
12 />
13 </div>
14);
15
16export default ProductPage;
步骤 7:创建友好的 URL 结构
Next.js 允许你使用嵌套文件夹和动态路由来创建友好的 URL 结构。确保你的 URL 简洁、易于理解,并包含重要的关键词。
示例:博客页面
1// pages/blog/[slug].js
2const PostPage = ({ post }) => (
3 <div>
4 <h1>{post.title}</h1>
5 <p>{post.content}</p>
6 </div>
7);
8
9export async function getStaticPaths() {
10 const res = await fetch('https://api.example.com/posts');
11 const posts = await res.json();
12
13 const paths = posts.map(post => ({
14 params: { slug: post.slug },
15 }));
16
17 return { paths, fallback: false };
18}
19
20export async function getStaticProps({ params }) {
21 const res = await fetch(`https://api.example.com/posts/${params.slug}`);
22 const post = await res.json();
23
24 return { props: { post } };
25}
26
27export default PostPage;
步骤 8:使用结构化数据(Schema.org)
结构化数据帮助搜索引擎更好地理解你的内容。你可以通过在页面 <head>
中添加 JSON-LD 格式的结构化数据来实现。
1import Head from 'next/head';
2
3const BlogPage = () => (
4 <>
5 <Head>
6 <script
7 type="application/ld+json"
8 dangerouslySetInnerHTML={{
9 __html: JSON.stringify({
10 "@context": "https://schema.org",
11 "@type": "Blog",
12 "name": "博客",
13 "url": "https://www.my-next-seo-project.com/blog",
14 }),
15 }}
16 />
17 </Head>
18 <h1>博客页面</h1>
19 </>
20);
21
22export default BlogPage;
步骤 9:测试 SEO 性能
通过 Google Search Console、Lighthouse 等工具测试你的 SEO 成效,确保页面在搜索引擎中能够良好显示。
总结
- 创建 Next.js 项目并安装依赖:使用
create-next-app
创建项目并安装next-seo
。 - 配置全局 SEO 设置:使用
_app.js
和_document.js
文件配置全局 SEO 元数据。 - 页面 SEO 优化:使用
next-seo
或next/head
为每个页面设置元信息、Open Graph、Twitter 卡片等。 - 服务器端渲染(SSR)和静态生成(SSG):根据页面内容的动态性,选择合适的渲染方法,以提高 SEO 效果。
- 图片优化:使用
next/image
自动优化图片,提升加载速度。 - URL 结构优化:通过动态路由创建简洁、含有关键词的 URL。
- 结构化数据:使用 JSON-LD 格式提供结构化数据,帮助搜索引擎理解内容。
- SEO 测试和优化:通过 SEO 工具测试页面性能并进行持续优化。
通过这些步骤,你可以将 Next.js 项目从 0 到 1 完成 SEO 优化,提升网站在搜索引擎中的排名和可见度。
Next.js SEO 相关面试题
- Next.js 如何提高 SEO 排名?
- Next.js 通过支持服务器端渲染(SSR)和静态生成(SSG),使得页面内容在服务器端渲染,能够让搜索引擎更容易抓取完整的 HTML 内容,从而提升 SEO 排名。
- 如何在 Next.js 中使用
**next/head**
进行 SEO 优化?- 使用
next/head
组件,可以为每个页面设置标题、描述、关键字和其他 meta 标签,帮助搜索引擎更好地理解页面内容。
- 使用
- 什么是
**getStaticProps**
和**getServerSideProps**
,它们如何影响 SEO?getStaticProps
用于静态生成页面,在构建时生成页面,页面加载速度快,适合静态内容。getServerSideProps
用于服务器端渲染,每次请求都会生成页面,适合动态内容。两者都可以帮助搜索引擎抓取完整的页面内容,从而优化 SEO。
- 如何使用 Next.js 生成动态路由的静态页面?
- 使用
getStaticPaths
获取动态路由的路径,并结合getStaticProps
来生成每个路径的静态页面,这样可以提高 SEO,减少加载时间。
- 使用
- 如何通过 Next.js 优化图片以提高 SEO?
- 使用 Next.js 提供的
next/image
组件,它可以自动优化图片的大小,支持懒加载,减少页面加载时间,提高页面性能,从而对 SEO 有帮助。
- 使用 Next.js 提供的
- Next.js 如何处理 URL 结构以提高 SEO?
- Next.js 通过文件和文件夹的嵌套结构生成简洁且含有关键词的 URL,这对于 SEO 至关重要。URL 应该简洁、描述性强,并包含页面的主要关键词。
- 如何使用结构化数据来提高页面的 SEO 表现?
- 结构化数据(如 JSON-LD 格式)可以帮助搜索引擎理解页面内容。例如,在页面
<head>
中嵌入结构化数据,可以提高页面在搜索结果中的展示效果,增加点击率。
- 结构化数据(如 JSON-LD 格式)可以帮助搜索引擎理解页面内容。例如,在页面
- Next.js 中如何处理 JavaScript 和 CSS 文件的加载,如何优化这些文件以提高 SEO?
- Next.js 自动进行代码拆分,按需加载 JavaScript 和 CSS 文件。通过懒加载和代码分割,确保页面首次加载速度更快,提升 SEO 性能。
- 如何在 Next.js 中做 SEO 性能优化?
- 使用静态生成(SSG)来减少服务器负担,提高页面加载速度;使用服务器端渲染(SSR)来生成动态内容;优化图片加载,减少资源文件大小;使用懒加载来延迟加载非必要资源。
- 什么是“预渲染”和“动态渲染”以及它们对 SEO 的影响?
- “预渲染”指的是提前生成页面的 HTML(如 SSR 和 SSG),确保搜索引擎能直接获取页面内容;“动态渲染”指的是服务器根据请求动态生成页面。这两者都能够提升页面的 SEO 排名,确保搜索引擎可以抓取到完整的内容。
- Next.js 如何提高 SEO 排名?
- Next.js 通过支持服务器端渲染(SSR)和静态生成(SSG),使得页面内容在服务器端渲染,能够让搜索引擎更容易抓取完整的 HTML 内容,从而提升 SEO 排名。
- 如何在 Next.js 中使用
**next/head**
进行 SEO 优化?- 使用
next/head
组件,可以为每个页面设置标题、描述、关键字和其他 meta 标签,帮助搜索引擎更好地理解页面内容。
- 使用
- 什么是
**getStaticProps**
和**getServerSideProps**
,它们如何影响 SEO?getStaticProps
用于静态生成页面,在构建时生成页面,页面加载速度快,适合静态内容。getServerSideProps
用于服务器端渲染,每次请求都会生成页面,适合动态内容。两者都可以帮助搜索引擎抓取完整的页面内容,从而优化 SEO。
- 如何使用 Next.js 生成动态路由的静态页面?
- 使用
getStaticPaths
获取动态路由的路径,并结合getStaticProps
来生成每个路径的静态页面,这样可以提高 SEO,减少加载时间。
- 使用
- 如何通过 Next.js 优化图片以提高 SEO?
- 使用 Next.js 提供的
next/image
组件,它可以自动优化图片的大小,支持懒加载,减少页面加载时间,提高页面性能,从而对 SEO 有帮助。
- 使用 Next.js 提供的
- Next.js 如何处理 URL 结构以提高 SEO?
- Next.js 通过文件和文件夹的嵌套结构生成简洁且含有关键词的 URL,这对于 SEO 至关重要。URL 应该简洁、描述性强,并包含页面的主要关键词。
- 如何使用结构化数据来提高页面的 SEO 表现?
- 结构化数据(如 JSON-LD 格式)可以帮助搜索引擎理解页面内容。例如,在页面
<head>
中嵌入结构化数据,可以提高页面在搜索结果中的展示效果,增加点击率。
- 结构化数据(如 JSON-LD 格式)可以帮助搜索引擎理解页面内容。例如,在页面
- Next.js 中如何处理 JavaScript 和 CSS 文件的加载,如何优化这些文件以提高 SEO?
- Next.js 自动进行代码拆分,按需加载 JavaScript 和 CSS 文件。通过懒加载和代码分割,确保页面首次加载速度更快,提升 SEO 性能。
- 如何在 Next.js 中做 SEO 性能优化?
- 使用静态生成(SSG)来减少服务器负担,提高页面加载速度;使用服务器端渲染(SSR)来生成动态内容;优化图片加载,减少资源文件大小;使用懒加载来延迟加载非必要资源。
- 什么是“预渲染”和“动态渲染”以及它们对 SEO 的影响?
- “预渲染”指的是提前生成页面的 HTML(如 SSR 和 SSG),确保搜索引擎能直接获取页面内容;“动态渲染”指的是服务器根据请求动态生成页面。这两者都能够提升页面的 SEO 排名,确保搜索引擎可以抓取到完整的内容。
前端单元测试详细步骤
Vue 3 + Pinia + VueUse,并选择使用 Vitest 作为单元测试框架、Cypress 作为端到端测试工具。下面是如何创建这些文件和进行实际测试的详细步骤。
1. 安装所需的依赖
首先,确保你已安装 **Vitest**
和 **Vue Test Utils**
进行单元测试。如果要使用 Cypress 进行 E2E 测试,也需要安装它们。
安装 **Vitest**
和 **Vue Test Utils**
:
1npm install --save-dev vitest @vue/test-utils
安装 **Cypress**
进行 E2E 测试:
1npm install --save-dev cypress
2. 配置 Vitest
在你的项目根目录下,创建或更新 **vitest.config.ts**
文件:
1import { defineConfig } from 'vitest/config'
2
3export default defineConfig({
4 test: {
5 globals: true, // 使用全局变量
6 environment: 'jsdom', // 模拟浏览器环境
7 }
8})
3. 创建单元测试文件
假设你有一个 **utils/format.ts**
文件,包含一个简单的 **formatUsername**
函数:
**utils/format.ts**
1export function formatUsername(name: string) {
2 return name.trim().toLowerCase();
3}
**创建测试文件 ****utils/format.test.ts**
- 在项目的
**tests**
目录下创建**format.test.ts**
文件:**tests/utils/format.test.ts**
1import { describe, it, expect } from 'vitest';
2import { formatUsername } from '@/utils/format';
3
4describe('formatUsername', () => {
5 it('应去除前后空格并转换为小写', () => {
6 expect(formatUsername(' Alice ')).toBe('alice');
7 });
8
9 it('应处理已经是小写的情况', () => {
10 expect(formatUsername('bob')).toBe('bob');
11 });
12});
运行测试
运行命令来测试你的代码:
1npx vitest run
4. 创建组件测试文件
假设你有一个 Vue 组件 **UserCard.vue**
,该组件显示用户名并允许修改它。
**components/UserCard.vue**
1<template>
2 <div>
3 <p>{{ username }}</p>
4 <button @click="changeName">Change</button>
5 </div>
6</template>
7
8<script setup>
9import { ref } from 'vue';
10
11const username = ref('Alice');
12
13function changeName() {
14 username.value = 'Bob';
15}
16</script>
**创建测试文件 ****components/UserCard.test.ts**
- 在项目的
**tests/components**
目录下创建**UserCard.test.ts**
文件:**tests/components/UserCard.test.ts**
1import { mount } from '@vue/test-utils';
2import { describe, it, expect } from 'vitest';
3import UserCard from '@/components/UserCard.vue';
4
5describe('UserCard.vue', () => {
6 it('应该渲染默认的用户名', () => {
7 const wrapper = mount(UserCard);
8 expect(wrapper.text()).toContain('Alice');
9 });
10
11 it('点击按钮后,用户名应该变为 Bob', async () => {
12 const wrapper = mount(UserCard);
13 await wrapper.find('button').trigger('click');
14 expect(wrapper.text()).toContain('Bob');
15 });
16});
运行测试
1npx vitest run
5. 测试 Pinia 状态管理
假设你有一个 Pinia store,管理用户信息:
**store/user.ts**
1import { defineStore } from 'pinia';
2import { ref } from 'vue';
3
4export const useUserStore = defineStore('user', () => {
5 const user = ref({ username: '' });
6
7 function setUser(username: string) {
8 user.value.username = username;
9 }
10
11 return { user, setUser };
12});
**创建测试文件 ****store/user.test.ts**
- 在项目的
**tests/store**
目录下创建**user.test.ts**
文件:**tests/store/user.test.ts**
1import { setActivePinia, createPinia } from 'pinia';
2import { useUserStore } from '@/store/user';
3import { describe, it, expect, beforeEach } from 'vitest';
4
5describe('User Store', () => {
6 beforeEach(() => {
7 setActivePinia(createPinia());
8 });
9
10 it('应该正确设置用户信息', () => {
11 const store = useUserStore();
12 store.setUser('Alice');
13 expect(store.user.username).toBe('Alice');
14 });
15});
运行测试
1npx vitest run
6. 创建端到端测试
假设你有一个简单的登录页面,用户可以输入用户名和密码进行登录。
**pages/Login.vue**
1<template>
2 <div>
3 <input type="text" v-model="username" placeholder="Username" />
4 <input type="password" v-model="password" placeholder="Password" />
5 <button @click="login">Login</button>
6 </div>
7</template>
8
9<script setup>
10import { ref } from 'vue';
11
12const username = ref('');
13const password = ref('');
14
15function login() {
16 if (username.value === 'admin' && password.value === '123456') {
17 window.location.href = '/dashboard';
18 }
19}
20</script>
**创建 E2E 测试文件 ****cypress/integration/login.spec.ts**
- 在
**cypress/integration**
目录下创建**login.spec.ts**
文件:**cypress/integration/login.spec.ts**
1describe('Login Page', () => {
2 it('登录成功后应跳转到 dashboard 页面', () => {
3 cy.visit('/login');
4 cy.get('input[type="text"]').type('admin');
5 cy.get('input[type="password"]').type('123456');
6 cy.get('button').click();
7 cy.url().should('include', '/dashboard');
8 });
9});
运行 Cypress 测试
1npx cypress open
然后选择 **login.spec.ts**
文件来运行测试。
7. 项目结构示例
1├── src
2│ ├── components
3│ │ └── UserCard.vue
4│ ├── store
5│ │ └── user.ts
6│ ├── utils
7│ │ └── format.ts
8│ └── pages
9│ └── Login.vue
10├── tests
11│ ├── components
12│ │ └── UserCard.test.ts
13│ ├── store
14│ │ └── user.test.ts
15│ ├── utils
16│ │ └── format.test.ts
17│ └── e2e
18│ └── login.spec.ts
19├── cypress
20│ └── integration
21│ └── login.spec.ts
22└── vitest.config.ts
总结
- 安装依赖**:首先确保安装了必要的依赖 (
**Vitest**
、**Vue Test Utils**
、**Cypress**
)。** - 单元测试**:对函数、方法进行独立测试,确保其逻辑正确。**
- 组件测试**:对 Vue 组件进行测试,模拟交互,确保组件功能正常。**
- 状态管理测试**:对 Pinia store 进行单元测试,验证状态和变更方法。**
- 端到端测试:使用 Cypress 进行真实场景下的用户操作测试,确保应用的流程顺畅。
前端测试相关面试题
前端测试是现代前端开发中不可或缺的一部分,面试中经常会涉及到相关的测试知识。以下是一些常见的 前端测试相关面试题,包括单元测试、组件测试、E2E 测试等方面,涵盖了工具(如 Vitest
、Jest
、Mocha
、Cypress
)以及测试技术的知识。
1. 单元测试相关
1.1 什么是单元测试?它有什么作用?
- 答案:单元测试是对代码中最小功能单元(通常是函数)进行验证的过程。其目的是确保代码中的每个单元都按预期工作。单元测试能够提高代码的稳定性、可维护性,并帮助开发人员在重构时避免出现错误。
1.2 你如何在 Vue 3 项目中进行单元测试?可以用哪些工具?
- 答案:在 Vue 3 项目中,常用的单元测试工具有:
Vue Test Utils
:Vue 官方提供的用于测试 Vue 组件的工具库。Vitest
或Jest
:用于运行测试和断言的测试框架。@testing-library/vue
:用于 Vue 组件的测试,侧重于用户交互和行为。- 例如,使用
mount
来挂载 Vue 组件,验证组件的渲染、行为等。
1.3 如何编写 Vue 组件的单元测试?
- 答案:在 Vue 组件的单元测试中,我们使用
mount
或shallowMount
来挂载组件,之后可以使用wrapper
来访问组件内部元素,并触发事件或进行断言。
例如,假设我们有一个 Counter
组件:
1<template>
2 <button @click="increment">{{ count }}</button>
3</template>
4
5<script setup>
6 import { ref } from 'vue';
7
8 const count = ref(0);
9 const increment = () => count.value++;
10</script>
组件的单元测试:
1import { mount } from '@vue/test-utils';
2import Counter from '@/components/Counter.vue';
3
4describe('Counter.vue', () => {
5 it('点击按钮时,应该增加计数', async () => {
6 const wrapper = mount(Counter);
7 await wrapper.find('button').trigger('click');
8 expect(wrapper.text()).toContain('1');
9 });
10});
1.4 什么是测试覆盖率?如何查看测试覆盖率?
- 答案:测试覆盖率是指代码库中被测试代码的比例。常见的测试覆盖率指标包括:
- 语句覆盖率:测试覆盖了多少行代码。
- 分支覆盖率:测试覆盖了多少分支(如条件语句的真/假路径)。
- 功能覆盖率:测试覆盖了多少功能。
在 Vitest
或 Jest
中,可以通过配置生成测试覆盖率报告。例如,在 Jest
中:
1npx jest --coverage
2. 组件测试相关
2.1 如何测试一个 Vue 组件的交互行为?
- 答案:Vue 组件的交互行为通常通过
trigger
方法来模拟用户事件(例如点击、输入等)。通过断言事件触发后的变化来验证组件是否按预期工作。
例如:
1<template>
2 <button @click="toggle">{{ message }}</button>
3</template>
4
5<script setup>
6 import { ref } from 'vue';
7
8 const message = ref('Click me');
9 const toggle = () => {
10 message.value = message.value === 'Click me' ? 'Clicked' : 'Click me';
11 };
12</script>
测试:
1import { mount } from '@vue/test-utils';
2import ToggleButton from '@/components/ToggleButton.vue';
3
4describe('ToggleButton.vue', () => {
5 it('点击按钮时,应该切换消息文本', async () => {
6 const wrapper = mount(ToggleButton);
7 expect(wrapper.text()).toContain('Click me');
8 await wrapper.find('button').trigger('click');
9 expect(wrapper.text()).toContain('Clicked');
10 });
11});
2.2 Vue 组件的生命周期钩子有哪些?它们在测试中有什么作用?
- 答案:Vue 3 中的生命周期钩子包括:
created
: 实例创建完成后调用。mounted
: DOM 挂载完成后调用。updated
: 组件更新后调用。unmounted
: 组件销毁时调用。
在测试中,这些生命周期钩子的作用可以帮助我们模拟组件的不同状态,验证组件行为在各个阶段的变化。
2.3 如何模拟 Vuex 或 Pinia store 中的状态来进行测试?
- 答案:可以通过
vitest
或jest
的 mock 功能模拟 Vuex 或 Pinia store。为确保测试的独立性,可以在测试时注入模拟的 store。
例如,使用 Pinia store:
1import { setActivePinia, createPinia } from 'pinia';
2import { useUserStore } from '@/store/user';
3import { mount } from '@vue/test-utils';
4import UserCard from '@/components/UserCard.vue';
5
6beforeEach(() => {
7 setActivePinia(createPinia());
8});
9
10it('should display the username from the store', () => {
11 const store = useUserStore();
12 store.setUser('Alice');
13 const wrapper = mount(UserCard);
14 expect(wrapper.text()).toContain('Alice');
15});
3. E2E 测试相关
3.1 你如何测试一个完整的登录流程?
- 答案:使用 E2E 测试工具(如 Cypress)模拟用户的登录行为,检查登录后的页面是否符合预期。
例如,使用 Cypress 测试登录:
1describe('Login Flow', () => {
2 it('should login and redirect to dashboard', () => {
3 cy.visit('/login');
4 cy.get('input[name="username"]').type('admin');
5 cy.get('input[name="password"]').type('123456');
6 cy.get('button[type="submit"]').click();
7 cy.url().should('include', '/dashboard');
8 });
9});
3.2 如何测试一个包含动态内容加载的页面?
- 答案:测试动态内容加载时,我们通常会使用 Cypress 等工具来模拟用户交互,验证异步请求和内容更新。
例如,测试页面加载数据:
1describe('Dynamic Content Loading', () => {
2 it('should load content dynamically', () => {
3 cy.visit('/dynamic-page');
4 cy.intercept('GET', '/api/data', { fixture: 'data.json' }).as('loadData');
5 cy.wait('@loadData');
6 cy.get('.content').should('contain', 'Some data');
7 });
8});
3.3 如何确保你的 E2E 测试是稳定的,避免出现闪烁或不确定性?
- 答案:为了确保 E2E 测试的稳定性,可以采取以下措施:
- 使用
**cy.wait()**
等待元素加载完成。 - 使用断言确保元素存在且可见。
- 使用 mock 请求,避免依赖外部 API。
- 确保测试的环境可控,如清理数据库、模拟数据等。
- 使用
4. 性能和测试相关
4.1 如何进行前端性能测试?
- 答案:可以使用浏览器的开发者工具(DevTools)中的性能面板进行性能分析,查看页面加载、渲染等的详细信息。此外,还可以使用工具如
Lighthouse
、WebPageTest
、Puppeteer
等进行自动化性能测试。
4.2 你如何测试一个页面的响应时间或加载时间?
- 答案:可以使用
Cypress
进行性能测试,模拟用户访问页面,并使用cy.window()
获取页面的加载时间,进行断言。例如:
1describe('Page Load Performance', () => {
2 it('should load the page within 2 seconds', () => {
3 cy.visit('/home');
4 cy.window().should('have.property', 'performance');
5 const [timing] = window.performance.getEntriesByType('navigation');
6 expect(timing.duration).to.be.lessThan(2000); // 响应时间小于 2 秒
7 });
8});
5. 调试与错误处理
5.1 如何调试单元测试中的失败案例?
- 答案:调试单元测试失败的常用方法:
- 检查测试的期望值和实际值是否匹配。
- 使用
console.log()
输出变量值,排查数据问题。 - 确保组件的 props、data、events 等正确传递和处理。
- 使用
debugger
或 IDE 的调试工具查看代码执行流程。
MySQL 常用 SQL 语句大全 🚀
1. 数据库操作
创建数据库
1CREATE DATABASE mydb;
删除数据库
1DROP DATABASE mydb;
使用数据库
1USE mydb;
2. 数据表操作
创建表
1CREATE TABLE users (
2 id INT PRIMARY KEY AUTO_INCREMENT,
3 name VARCHAR(50) NOT NULL,
4 age INT,
5 email VARCHAR(100) UNIQUE,
6 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
7);
查看表结构
1DESC users;
修改表
添加字段:
1ALTER TABLE users ADD COLUMN phone VARCHAR(15);
修改字段类型:
1ALTER TABLE users MODIFY COLUMN age SMALLINT;
删除字段:
1ALTER TABLE users DROP COLUMN phone;
修改表名:
1ALTER TABLE users RENAME TO customers;
删除表
1DROP TABLE users;
3. 数据操作
插入数据
1INSERT INTO users (name, age, email) VALUES ('Alice', 25, 'alice@example.com');
批量插入
1INSERT INTO users (name, age, email) VALUES
2('Bob', 30, 'bob@example.com'),
3('Charlie', 22, 'charlie@example.com');
查询数据
查询所有字段:
1SELECT * FROM users;
查询指定字段:
1SELECT name, email FROM users;
查询去重数据:
1SELECT DISTINCT age FROM users;
查询带条件的数据:
1SELECT * FROM users WHERE age > 25;
模糊查询 (**LIKE**
):
1SELECT * FROM users WHERE name LIKE 'A%'; -- 以A开头
2SELECT * FROM users WHERE name LIKE '%e'; -- 以e结尾
3SELECT * FROM users WHERE name LIKE '%li%'; -- 包含li
范围查询 (**BETWEEN**
** 和 **IN**
)**:
1SELECT * FROM users WHERE age BETWEEN 20 AND 30;
2SELECT * FROM users WHERE age IN (22, 25, 30);
排序 (**ORDER BY**
):
1SELECT * FROM users ORDER BY age ASC; -- 按年龄升序
2SELECT * FROM users ORDER BY age DESC; -- 按年龄降序
分页 (**LIMIT**
):
1SELECT * FROM users LIMIT 10; -- 取前10条
2SELECT * FROM users LIMIT 10 OFFSET 20; -- 跳过20条后取10条
更新数据
1UPDATE users SET age = 28 WHERE name = 'Alice';
删除数据
1DELETE FROM users WHERE age < 18;
清空表(保留结构,不记录日志,性能快):
1TRUNCATE TABLE users;
4. 关联查询(JOIN)
**创建另一张表 ****orders**
1CREATE TABLE orders (
2 id INT PRIMARY KEY AUTO_INCREMENT,
3 user_id INT,
4 amount DECIMAL(10,2),
5 order_date DATE,
6 FOREIGN KEY (user_id) REFERENCES users(id)
7);
INNER JOIN(内连接)
查询所有用户及其订单:
1SELECT users.name, orders.amount, orders.order_date
2FROM users
3INNER JOIN orders ON users.id = orders.user_id;
LEFT JOIN(左连接)
查询所有用户,即使没有订单:
1SELECT users.name, orders.amount, orders.order_date
2FROM users
3LEFT JOIN orders ON users.id = orders.user_id;
RIGHT JOIN(右连接)
查询所有订单,即使没有对应的用户:
1SELECT users.name, orders.amount, orders.order_date
2FROM users
3RIGHT JOIN orders ON users.id = orders.user_id;
5. 统计 & 聚合
计数 (**COUNT**
)
1SELECT COUNT(*) FROM users; -- 统计总人数
2SELECT COUNT(*) FROM users WHERE age > 25; -- 统计年龄大于25的人数
求和 (**SUM**
)
1SELECT SUM(amount) FROM orders; -- 计算订单总金额
平均值 (**AVG**
)
1SELECT AVG(age) FROM users; -- 计算平均年龄
最大 & 最小值 (**MAX**
** / **MIN**
)**
1SELECT MAX(age) FROM users; -- 最大年龄
2SELECT MIN(age) FROM users; -- 最小年龄
分组统计 (**GROUP BY**
)
1SELECT age, COUNT(*) FROM users GROUP BY age; -- 按年龄分组统计人数
HAVING 过滤分组数据:
1SELECT age, COUNT(*) FROM users GROUP BY age HAVING COUNT(*) > 1;
6. 子查询
查询年龄最大的人
1SELECT * FROM users WHERE age = (SELECT MAX(age) FROM users);
查询有订单的用户
1SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
7. 事务控制
开启事务:
1START TRANSACTION;
插入数据(若出错回滚):
1INSERT INTO users (name, age, email) VALUES ('David', 26, 'david@example.com');
2ROLLBACK; -- 撤销操作
提交事务:
1COMMIT;
8. 索引优化
创建索引
1CREATE INDEX idx_users_email ON users(email);
删除索引
1DROP INDEX idx_users_email ON users;
9. 视图
创建视图
1CREATE VIEW user_orders AS
2SELECT users.name, orders.amount, orders.order_date
3FROM users
4JOIN orders ON users.id = orders.user_id;
使用视图
1SELECT * FROM user_orders;
删除视图
1DROP VIEW user_orders;
10. 用户 & 权限
创建用户
1CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
授权
1GRANT ALL PRIVILEGES ON mydb.* TO 'newuser'@'localhost';
查看权限
1SHOW GRANTS FOR 'newuser'@'localhost';
撤销权限
1REVOKE ALL PRIVILEGES ON mydb.* FROM 'newuser'@'localhost';
删除用户
1DROP USER 'newuser'@'localhost';
11. 备份 & 恢复
备份数据库
1mysqldump -u root -p mydb > mydb_backup.sql
恢复数据库
1mysql -u root -p mydb < mydb_backup.sql
项目
权限代码解析
通过前后端分离 + 动态权限路由的方式来实现权限控制,具体实现方式如下:
1. 主要逻辑
- 前端路由权限控制
- 前端定义基础路由(constantRoutes):这些是所有用户都能访问的基础路由。
- 后端返回用户可访问的菜单(listMenus):后端会返回用户的菜单数据,前端根据这个菜单数据来匹配对应的动态路由。
- 路由权限匹配
- 代码中使用
filterAsyncRoutes(menus, asyncRoutes)
方法,通过后端返回的菜单,从asyncRoutes
中筛选用户有权限的路由。 hasPermission(menus, route, path)
方法会检查用户角色是否匹配route.meta.roles
,或者检查menus
里是否包含route.path
。- 只有匹配成功的路由,才会最终添加到
routes
里,用户才能访问。
- 代码中使用
- 支持三级菜单
setMenuTabs(menus)
方法会处理三级菜单,对菜单数据进行转换,使其支持深层级嵌套。
- 权限数据来源
listMenus()
:从后端获取用户菜单列表,匹配路由权限。listRoutes()
(被注释掉了):可能是原来用来获取后端定义的路由,现在可能已经改成菜单管理。
2. 权限控制方式
(1)角色权限控制
- 白名单角色(ROOT、SUPER、ADMIN、STATIC) 直接拥有所有权限,不受限制:
1const whiteRoles = ["root", "super", "admin", "static"];
2const hasPermission = (menus: object[], route: RouteRecordRaw, path?: string) => {
3 return (
4 whiteRoles.some((role) => route.meta?.roles?.includes(role)) ||
5 menus.findIndex((menu) => menu.link && menu.link === path) > -1
6 );
7};
如果当前用户角色是白名单角色,直接跳过权限校验,拥有所有权限。
(2)动态菜单匹配
- 代码中的
listMenus()
是从后端获取菜单的 API:
1listMenus().then(({ data }) => {
2 const accessedRoutes = filterAsyncRoutes(flatMenus(data.rows), asyncRoutes);
3 menuList.value = data.rows;
4 setRoutes(accessedRoutes);
5 resolve(accessedRoutes);
6});
后端返回的菜单会经过 flatMenus(data.rows)
处理,把多层级菜单压平,然后和 asyncRoutes
进行匹配,生成当前用户可以访问的路由。
(3)深度遍历过滤权限
- 代码中
filterAsyncRoutes()
方法支持递归遍历,确保子路由也能匹配权限:
1const filterAsyncRoutes = (menus, routes, basePath, depth = 1) => {
2 const asyncRoutesMap = [];
3 routes.forEach((route) => {
4 const tmpRoute = { ...route };
5 const path = resolvePath(tmpRoute.path, basePath);
6 if (hasPermission(menus, tmpRoute, path)) {
7 for (const menu of menus) {
8 if (menu.link === path) {
9 tmpRoute.menuId = menu.menuId || menu.menusid;
10 tmpRoute.menuName = menu.menuName || menu.name;
11 tmpRoute.meta.count = 0;
12 // 三级菜单和功能模块匹配
13 const tabs = menu.children.filter((child) => child.sortno && !child.link);
14 if (tabs.length > 0) {
15 tmpRoute.tabList = setMenuTabs(tabs);
16 }
17 const funcs = menu.children.filter((child) => !child.sortno && !child.link);
18 if (funcs.length > 0) {
19 tmpRoute.funcList = funcs;
20 }
21 break;
22 }
23 }
24 if (depth < 2 && tmpRoute.children?.length) {
25 tmpRoute.children = filterAsyncRoutes(menus, tmpRoute.children, path, depth + 1);
26 }
27 asyncRoutesMap.push(tmpRoute);
28 }
29 });
30 return asyncRoutesMap;
31};
1- **第一层遍历路由**
2- **匹配 **`**menu.link === route.path**`**,绑定菜单 ID、名称、待办数量**
3- **如果有三级菜单,进行二次处理**
4- **深度递归遍历,最多支持两层**
3. 权限流程总结
(1)登录后,前端获取菜单
listMenus()
从后端获取用户菜单flatMenus()
压平菜单数据filterAsyncRoutes()
匹配用户权限setRoutes()
设置最终路由menuList.value
存储菜单
(2)前端路由匹配
- 角色匹配 (
**whiteRoles**
)
超级管理员直接放行。 - 菜单路径匹配 (
**menus.findIndex(menu.link === path) > -1**
)
普通用户根据后端返回的菜单判断是否有权限。 - 支持三级菜单 (
**setMenuTabs**
)
三级菜单的数据会被重新整理,提供tabList
和funcList
。
4. 可能的扩展点
- 接口权限控制
- 目前只基于菜单控制权限,可能还需要结合后端 API 权限(比如
button
级权限)。
- 目前只基于菜单控制权限,可能还需要结合后端 API 权限(比如
- 前端缓存优化
- 菜单数据可以存 Vuex/Pinia 或
localStorage
,减少listMenus()
的请求次数。
- 菜单数据可以存 Vuex/Pinia 或
- 多角色支持
- 目前只支持单角色匹配,如果用户有多个角色,需要调整
hasPermission()
逻辑。
- 目前只支持单角色匹配,如果用户有多个角色,需要调整
5. 总结
你们公司的前端权限控制方案是:
- 基于角色(root/super/admin/static 直接放行)。
- 后端返回菜单数据(
listMenus()
)。 - 前端过滤动态路由(
filterAsyncRoutes()
)。 - 支持三级菜单(
setMenuTabs()
)。 - 最终匹配后,动态添加路由(
setRoutes()
)。
这套方式比较常见,适用于前后端分离的权限管理系统,但可以考虑补充API 按钮级权限和本地缓存优化。
1import { RouteRecordRaw } from "vue-router";
2import { defineStore } from "pinia";
3import { constantRoutes, asyncRoutes } from "@/router";
4import path from "path-browserify";
5import { store } from "@/store";
6import { listRoutes, listMenus } from "@/api/menu";
7
8// const modules = import.meta.glob("../../views/**/**.vue");
9// const Layout = () => import("@/layout/index.vue");
10
11/**
12 * Use meta.role to determine if the current user has permission
13 *
14 * @param roles 用户角色集合
15 * @param route 路由
16 * @returns
17 */
18// const hasPermission = (roles: string[], route: RouteRecordRaw) => {
19// if (route.meta && route.meta.roles) {
20// // 角色【超级管理员】拥有所有权限,忽略校验
21// if (roles.includes("ROOT")) {
22// return true;
23// }
24// return roles.some((role) => {
25// if (route.meta?.roles) {
26// return route.meta.roles.includes(role);
27// }
28// });
29// }
30// return false;
31// };
32
33/**
34 * Use meta.role to determine if the current user has permission
35 *
36 * @param menus 菜单集合
37 * @param route 路由
38 * @param path 路由路径
39 * @returns
40 */
41const whiteRoles = ["root", "super", "admin", "static"];
42const hasPermission = (menus: object[], route: RouteRecordRaw, path?: string) => {
43 return (
44 whiteRoles.some((role) => {
45 if (route.meta?.roles) {
46 return route.meta.roles.includes(role);
47 }
48 }) || menus.findIndex((menu) => menu.link && menu.link === path) > -1
49 );
50};
51
52/**
53 * 生成路径
54 *
55 * @param paths 路径
56 * @param basePath 基础路径
57 * @returns
58 */
59const resolvePath = (paths: string, basePath = "") => {
60 return paths ? path.resolve(basePath, paths) : "";
61};
62
63/**
64 * 压平菜单
65 *
66 * @param menus 菜单集合
67 * @param basePath 基础路径
68 * @returns
69 */
70const flatMenus = (menus: object[], basePath?: string) => {
71 let res: object[] = [];
72 for (const menu of menus) {
73 menu.link = resolvePath(menu.linkrouter, basePath);
74 menu.children = menu.children || menu.childList || menu.subMenuList || [];
75 res.push(menu);
76 if (menu.children.length) {
77 res = res.concat(flatMenus(menu.children, menu.link));
78 }
79 }
80 return res;
81};
82
83/**
84 * 设置三级菜单
85 *
86 * @param menus 菜单集合
87 * @returns
88 */
89
90const setMenuTabs = (menus: object[]) => {
91 //三级菜单
92 return menus.map((child) => {
93 return {
94 ...child,
95 menuId: child.menuId || child.menusid,
96 menuName: child.menuName || child.name,
97 menuSort: child.menuSort || child.sortno,
98 };
99 });
100};
101
102/**
103 * 递归过滤有权限的异步(动态)路由
104 *
105 * @param routes 接口返回的异步(动态)路由
106 * @param roles 用户角色集合
107 * @returns 返回用户有权限的异步(动态)路由
108 */
109// const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
110// const asyncRoutes: RouteRecordRaw[] = [];
111
112// routes.forEach((route) => {
113// const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
114// if (!route.name) {
115// tmpRoute.name = route.path;
116// }
117// // 判断用户(角色)是否有该路由的访问权限
118// if (hasPermission(roles, tmpRoute)) {
119// if (tmpRoute.component?.toString() == "Layout") {
120// tmpRoute.component = Layout;
121// } else {
122// const component = modules[`../../views/${tmpRoute.component}.vue`];
123// if (component) {
124// tmpRoute.component = component;
125// } else {
126// tmpRoute.component = modules[`../../views/error-page/404.vue`];
127// }
128// }
129
130// if (tmpRoute.children) {
131// tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
132// }
133
134// asyncRoutes.push(tmpRoute);
135// }
136// });
137
138// return asyncRoutes;
139// };
140
141/**
142 * 递归过滤有权限的异步(动态)路由
143 *
144 * @param menus 接口返回的菜单
145 * @param routes 异步路由表
146 * @param basePath 基础路径
147 * @returns 返回用户有权限的异步(动态)路由
148 */
149const filterAsyncRoutes = (
150 menus: object[],
151 routes: RouteRecordRaw[],
152 basePath?: string,
153 depth = 1
154) => {
155 const asyncRoutesMap: RouteRecordRaw[] = [];
156 routes.forEach((route) => {
157 const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
158 const path = resolvePath(tmpRoute.path, basePath);
159 if (hasPermission(menus, tmpRoute, path)) {
160 for (const menu of menus) {
161 if (menu.link === path) {
162 //统一字段名称
163 tmpRoute.menuId = menu.menuId || menu.menusid;
164 tmpRoute.menuName = menu.menuName || menu.name;
165 tmpRoute.meta.count = 0; //代办事项数量
166 //设置三级菜单
167 const tabs = menu.children.filter((child) => child.sortno && !child.link);
168 if (tabs.length > 0) {
169 tmpRoute.tabList = setMenuTabs(tabs);
170 }
171 //设置功能模块
172 const funcs = menu.children.filter((child) => !child.sortno && !child.link);
173 if (funcs.length > 0) {
174 tmpRoute.funcList = funcs;
175 }
176 break;
177 }
178 }
179 // (setMenuTabs)(menus, tmpRoute, path);
180 //这里的depth只处理到第二层
181 if (depth < 2 && tmpRoute.children?.length) {
182 tmpRoute.children = filterAsyncRoutes(menus, tmpRoute.children, path, depth + 1);
183 }
184 asyncRoutesMap.push(tmpRoute);
185 }
186 });
187 return asyncRoutesMap;
188};
189
190// setup
191export const usePermissionStore = defineStore("permission", () => {
192 // state
193 const routes = ref<RouteRecordRaw[]>([]);
194 const menuList = ref([]); //菜单
195 const menuTabs = ref<object[]>([]); //三级菜单
196 const jobCount = ref({});
197 const todoMap = computed(() => {
198 const relation = {
199 //一级二级菜单
200 urgentAffair: ["matterTotal"],
201 //三级菜单
202 急办事项: "matterTotal",
203 };
204 return new Map(Object.entries(relation));
205 });
206
207 // actions
208 function setRoutes(newRoutes: RouteRecordRaw[]) {
209 routes.value = constantRoutes.concat(newRoutes.sort((a, b) => a.meta.sort - b.meta.sort));
210 }
211
212 //设置三级菜单
213 function setMenuTabs(tabs: object[]) {
214 menuTabs.value = tabs;
215 }
216 /**
217 * 生成动态路由
218 *
219 * @param roles 用户角色集合
220 * @returns
221 */
222 function generateRoutes(roles: string[]) {
223 return new Promise<RouteRecordRaw[]>((resolve, reject) => {
224 // 接口获取所有路由
225 // listRoutes()
226 // .then(({ data: asyncRoutes }) => {
227 // // 根据角色获取有访问权限的路由
228 // const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
229 // setRoutes(accessedRoutes);
230 // resolve(accessedRoutes);
231 // })
232 // .catch((error) => {
233 // reject(error);
234 // });
235 // 接口获取所有菜单
236 listMenus()
237 .then( ({ data }) => {
238 // 根据角色获取有访问权限的路由
239 const accessedRoutes = filterAsyncRoutes(flatMenus(data.rows), asyncRoutes);
240 menuList.value = data.rows;
241 setRoutes(accessedRoutes);
242 resolve(accessedRoutes);
243 })
244 .catch((error) => {
245 reject(error);
246 });
247 });
248 }
249
250 /**
251 * 混合模式左侧菜单
252 */
253 const mixLeftMenu = ref<RouteRecordRaw[]>([]);
254 function getMixLeftMenu(activeTop: string) {
255 routes.value.forEach((item) => {
256 if (item.path === activeTop) {
257 mixLeftMenu.value = item.children || [];
258 }
259 });
260 }
261 return {
262 routes,
263 setRoutes,
264 generateRoutes,
265 getMixLeftMenu,
266 mixLeftMenu,
267 menuList,
268 menuTabs,
269 setMenuTabs,
270 jobCount,
271 todoMap,
272 };
273});
274
275// 非setup
276export function usePermissionStoreHook() {
277 return usePermissionStore(store);
278}
你的公司路由权限主要是通过 后端返回菜单数据,然后前端根据菜单数据生成动态路由 来实现的。面试时,可以从以下几个方面回答:
项目权限回答
1. 总体架构
我们的路由权限是基于 Vue 3 + Vue Router + Pinia 来管理的,权限控制方式是 后端返回菜单权限,前端根据后端返回的菜单 动态生成可访问的路由。
2. 详细流程
① 后端返回菜单权限
- 我们的权限管理是基于菜单权限控制的,后端返回
listMenus
这个 API,包含当前用户可访问的菜单数据。 - 菜单数据是一个嵌套的结构,包含:
link
: 菜单对应的路由路径childList/subMenuList
: 子菜单(嵌套结构)sortno
: 排序字段- 其他业务字段(如
menuId
、menuName
等)。
② 解析菜单并构建动态路由
- 前端会 将菜单数据扁平化(
flatMenus
方法),然后递归匹配异步路由,只保留当前用户有权限的路由:
1const accessedRoutes = filterAsyncRoutes(flatMenus(data.rows), asyncRoutes);
filterAsyncRoutes
方法:- 遍历
asyncRoutes
路由表,检查是否存在menu.link
,如果存在,说明该用户有访问权限。 - 处理
meta.roles
角色权限(ROOT、ADMIN 这些白名单用户不受限制)。 - 递归处理子路由,保留符合权限的路由。
- 遍历
③ 生成最终的动态路由
- 最终,我们会将
constantRoutes
(静态路由)和accessedRoutes
(动态路由)合并:
1setRoutes(constantRoutes.concat(accessedRoutes));
- 这样,Vue Router 的
routes
就是动态计算的,确保了用户只能访问有权限的页面。
3. 角色权限控制
① 角色白名单
- 代码中有一个
whiteRoles
(白名单角色):
1const whiteRoles = ["root", "super", "admin", "static"];
1- 如果用户的 `role` 在 `whiteRoles` 里,就可以访问所有路由。
② 基于 meta.roles
过滤
hasPermission
方法会判断路由的meta.roles
是否包含当前用户的角色:
1if (route.meta?.roles) {
2 return route.meta.roles.includes(role);
3}
③ 多级权限管理
- 代码支持三级菜单:
1const tabs = menu.children.filter((child) => child.sortno && !child.link);
2tmpRoute.tabList = setMenuTabs(tabs);
1- 一级、二级菜单是 `listMenus` 直接返回的。
2- 三级菜单会从 `children` 中筛选 `sortno` 字段。
4. 面试回答示例
Q: 你们项目的权限管理是怎么做的?
A:
我们采用基于后端返回菜单权限的方式进行权限管理,具体流程如下:
- 后端返回菜单数据,包含当前用户可访问的
menuId
、link
、子菜单等信息。 - 前端解析菜单,使用
flatMenus
方法将菜单扁平化,并递归匹配asyncRoutes
路由表,筛选出用户有权限访问的路由。 - Vue Router 动态生成路由,通过
setRoutes
方法合并constantRoutes
(静态路由)和accessedRoutes
(动态路由),确保用户只能访问有权限的页面。 - 支持角色权限控制,对于
root
、admin
这些白名单角色,无需校验权限;其他用户则根据meta.roles
和menu.link
进行权限判断。
5. 可能的追问
Q1. 你们的权限控制是前端还是后端做的?
- 我们的权限是 后端控制菜单,前端动态生成路由,这种方式可以:
- 灵活性高:后端可以直接修改用户权限,而不需要修改前端代码。
- 降低前端维护成本:前端只需处理
menu.link
关联vue-router
的路由。
Q2. 你们如何控制不同角色访问不同页面?
- 主要有两个方式:
- **基于 **
**meta.roles**
:如果meta.roles
里没有当前用户角色,则无法访问该路由。 - **基于 **
**menu.link**
:如果menu.link
没有匹配asyncRoutes
,则该页面不会出现在vue-router
里,相当于没有权限。
- **基于 **
Q3. 你们的权限管理是否支持动态菜单?
- 支持,因为菜单是 后端返回的,前端动态计算,所以:
- 当用户权限变更后,只需重新请求
listMenus
,然后generateRoutes()
重新计算路由。 - Vue Router 会自动更新,用户无须刷新页面即可看到权限变更。
- 当用户权限变更后,只需重新请求
Q4. 你们是如何处理三级菜单的?
- 代码里有
setMenuTabs
处理三级菜单:
1const tabs = menu.children.filter((child) => child.sortno && !child.link);
2tmpRoute.tabList = setMenuTabs(tabs);
1- **二级菜单**有 `link`,用于 `vue-router` 解析。
2- **三级菜单**通常没有 `link`,只作为 `tab` 形式展示,存储在 `tabList` 里。
总结
- 后端返回菜单,前端根据
menu.link
和asyncRoutes
动态生成路由。 - 支持角色权限,白名单角色 (
root
,admin
) 直接放行,其他角色根据meta.roles
过滤。 - 支持三级菜单,非
link
的菜单作为tabList
处理。 - 支持动态权限更新,用户权限变更后只需重新请求
listMenus
,即可实时更新路由。
你可以根据面试情况调整回答,重点突出后端控制菜单 + 前端动态生成路由的方案,清晰表达实现方式就能加分!
按钮权限
自定义 Vue 指令 (**v-hasPerm**
** 和 **v-hasRole**
)** 来控制的,具体流程如下:
1. 按钮权限 (v-hasPerm
)
核心逻辑:
- **从用户信息中获取
**roles**
和 ****perms**
:
1const { roles, perms } = useUserStoreHook().user;
- 超级管理员 (
**ROOT**
) 拥有所有权限:
1if (roles.includes("ROOT")) {
2 return true;
3}
- 校验当前按钮是否有绑定的权限:
1const requiredPerms = binding.value;
2const hasPerm = perms?.some((perm) => {
3 return requiredPerms.includes(perm);
4});
1- `binding.value` 代表 `v-hasPerm="['sys:menu:add']"` 里的权限数组。
2- `perms` 代表当前用户的权限列表(从 `useUserStoreHook().user` 获取)。
3- `some()` 判断用户权限列表中是否包含该按钮需要的权限。
- 没有权限就移除该按钮:
1if (!hasPerm) {
2 el.parentNode && el.parentNode.removeChild(el);
3}
示例
1<el-button v-hasPerm="['sys:menu:add']">新增菜单</el-button>
- 如果
**useUserStoreHook().user.perms**
里包含**sys:menu:add**
,按钮会显示; - 否则,按钮会被
**el.parentNode.removeChild(el)**
移除。
2. 角色权限 (v-hasRole
)
核心逻辑:
- **从用户信息中获取 **
**roles**
:
1const { roles } = useUserStoreHook().user;
- 校验当前用户是否包含
**binding.value**
指定的角色:
1const requiredRoles = binding.value;
2const hasRole = roles.some((role) => {
3 return requiredRoles.includes(role);
4});
1- `binding.value` 代表 `v-hasRole="['admin','test']"` 里的角色数组。
2- `roles` 是当前用户的角色列表。
3- `some()` 判断当前用户是否包含某个角色。
- 没有权限就移除该按钮:
1if (!hasRole) {
2 el.parentNode && el.parentNode.removeChild(el);
3}
示例
1<el-button v-hasRole="['admin']">管理员操作</el-button>
- 如果
**useUserStoreHook().user.roles**
里包含**admin**
,按钮会显示; - 否则,按钮会被移除。
3. 权限管理的整体流程
- **用户登录后,后端返回
**roles**
和 ****perms**
- 角色示例:
1{
2 "roles": ["admin"],
3 "perms": ["sys:menu:add", "sys:menu:edit"]
4}
- 前端
**useUserStoreHook**
存储用户权限信息- 用户权限存入
Pinia
,前端可以随时访问。
- 用户权限存入
- Vue 自定义指令
**v-hasPerm**
和**v-hasRole**
负责控制按钮权限。v-hasPerm
控制 按钮是否显示;v-hasRole
控制 角色是否可见。
4. 面试回答示例
Q1: 你们的按钮权限是怎么控制的?A:
我们的按钮权限是基于 自定义 Vue 指令 (**v-hasPerm**
和 **v-hasRole**
) 实现的:
- 用户登录后,后端返回用户的
roles
(角色)和perms
(权限)。 useUserStoreHook().user
存储这些权限信息。v-hasPerm
指令会检查binding.value
是否包含在perms
里,如果没有权限,就从 DOM 中移除该按钮。v-hasRole
指令检查roles
是否匹配binding.value
,如果不匹配,也会移除该按钮。
**Q2: 你们如何控制不同角色的按钮权限?**A:
我们主要依靠 v-hasPerm
和 v-hasRole
:
**v-hasPerm**
** 控制按钮级别权限**,如:
1<el-button v-hasPerm="['sys:menu:add']">新增菜单</el-button>
1- 只有 `perms` 里包含 `sys:menu:add` 的用户,才能看到按钮。
**v-hasRole**
** 控制角色级别权限**,如:
1<el-button v-hasRole="['admin']">管理员操作</el-button>
1- 只有 `roles` 里有 `admin` 的用户才能看到按钮。
**Q3: 你们权限管理的优点是什么?**A:
我们的权限管理 基于前后端分离,有以下优点:
- 后端统一管理权限,前端只负责显示和隐藏,维护成本低。
- 支持角色和权限双重控制,更灵活:
v-hasRole
适用于 角色级权限(如管理员才能操作)。v-hasPerm
适用于 细粒度的按钮权限(如新增、删除、编辑)。
- 权限动态更新:
- 用户权限变更后,重新请求
userInfo
,Vue 组件会自动刷新权限。
- 用户权限变更后,重新请求
总结
你的权限管理 基于 Vue 3 自定义指令 + Pinia,主要包含:
**v-hasPerm**
:判断perms
是否匹配,决定是否显示按钮。**v-hasRole**
:判断roles
是否匹配,决定是否显示按钮。- 后端控制权限,前端动态显示,保证安全性和灵活性。
代码
1import { useUserStoreHook } from "@/store/modules/user";
2import { Directive, DirectiveBinding } from "vue";
3
4/**
5 * 按钮权限
6 */
7export const hasPerm: Directive = {
8 mounted(el: HTMLElement, binding: DirectiveBinding) {
9 // 「超级管理员」拥有所有的按钮权限
10 const { roles, perms } = useUserStoreHook().user;
11 if (roles.includes("ROOT")) {
12 return true;
13 }
14 // 「其他角色」按钮权限校验
15 const { value } = binding;
16 if (value) {
17 const requiredPerms = value; // DOM绑定需要的按钮权限标识
18
19 const hasPerm = perms?.some((perm) => {
20 return requiredPerms.includes(perm);
21 });
22
23 if (!hasPerm) {
24 el.parentNode && el.parentNode.removeChild(el);
25 }
26 } else {
27 throw new Error(
28 "need perms! Like v-has-perm=\"['sys:user:add','sys:user:edit']\""
29 );
30 }
31 },
32};
33
34/**
35 * 角色权限
36 */
37export const hasRole: Directive = {
38 mounted(el: HTMLElement, binding: DirectiveBinding) {
39 const { value } = binding;
40
41 if (value) {
42 const requiredRoles = value; // DOM绑定需要的角色编码
43 const { roles } = useUserStoreHook().user;
44 const hasRole = roles.some((perm) => {
45 return requiredRoles.includes(perm);
46 });
47
48 if (!hasRole) {
49 el.parentNode && el.parentNode.removeChild(el);
50 }
51 } else {
52 throw new Error("need roles! Like v-has-role=\"['admin','test']\"");
53 }
54 },
55};
用户信息存储
在我们的 Vue 3 项目中,我们使用 Pinia
进行 用户状态管理,结合 VueUse
提供的 useStorage
进行数据持久化,确保用户信息在页面刷新后仍然可用。
1. 用户信息存储方式
- Token(
accessToken
):- 采用
useStorage("accessToken", "")
存储,使用**localStorage**
** 持久化**,保证刷新页面后仍可用。 - 登录后,后端返回
Bearer
+qxToken
作为Authorization
,存入token.value
。
- 采用
- 用户基础信息(
user
):- 采用
ref<UserInfo>
存储,存放在 Pinia Store 的状态 中,不直接持久化,避免过多依赖本地存储。 - 通过
getUserInfoApi()
获取,存入user.value
,数据包括userId
、roles
、perms
等。
- 采用
- 账号记录(
userAccounts
):- 采用
useStorage("userAccounts", [])
存储,保存在 localStorage,用于快速切换账号。 - 只保留 最近 7 个 账号,避免存储过多无用数据。
- 密码 采用
btoa()
Base64 编码(但目前仅作占位,实际生产环境应使用 AES 或 RSA 加密)。
- 采用
2. 安全性措施
- Token 机制:
- 存储
accessToken
主要用于请求认证,存于localStorage
,避免sessionStorage
导致页面关闭后丢失,但可能面临 XSS 风险,生产环境应考虑 HttpOnly Cookie 方案。
- 存储
- 账号密码存储:
- 目前使用
btoa()
进行 Base64 编码,但生产环境需要更安全的 AES/RSA 加密,防止明文泄露。
- 目前使用
- 用户信息格式化:
formatFields(data)
统一字段命名,防止后端接口字段变更导致前端代码异常,增强兼容性。
- Token 失效处理:
- 如果
getUserInfo()
检测到 Token 失效(如code === 10105/10106
),会 自动清除 Token 并重定向至登录页。
- 如果
3. 用户数据清理
- 登出时:
logout()
会 清空 Token 并刷新页面 (location.reload()
),确保用户数据清理完全。
- 重置 Token:
resetToken()
仅清除token
,但不会刷新页面,适用于 Token 过期的情况。
- 重置角色:
resetRoles()
清空user.roles
,适用于用户切换角色或权限变更的情况。
总结
我们的用户信息存储方案具备: ✅ 持久化存储(useStorage
)保证数据不丢失
✅ 安全性考虑(Token 处理、加密账号存储、Token 失效处理)
✅ 良好的状态管理(Pinia + ref
动态更新)
axios 封装
在项目中,为了提高代码的复用性、增强可维护性以及更好地处理请求与响应逻辑,我们对 axios
进行了封装,主要包含以下几个方面:
1. 创建 Axios 实例
我们通过 axios.create()
方法创建了一个 service
实例,配置了:
**baseURL**
:根据import.meta.env
选择不同的 API 地址,适配开发环境与生产环境。**timeout**
:设置请求超时时间,防止请求无限等待。**headers**
:默认使用"application/json;charset=utf-8"
,并在transformRequest
中处理Content-Type
逻辑:- 如果
Content-Type
是text/json/xml/html/javascript/form-data
,则直接发送原始数据。 - 其他情况默认使用
qs.stringify()
序列化数据,content-type
自动变为application/x-www-form-urlencoded
。
- 如果
2. 请求拦截器
我们在 request.interceptors.use()
中:
- 实现并发请求控制:
- 通过
limitRequest.add()
机制,确保请求在受控情况下并发执行,防止短时间内大量相同请求发送。
- 通过
- Token 处理:
- 通过
useUserStoreHook()
获取token
,如果用户已登录,可在请求头中携带Authorization
。
- 通过
- 取消重复请求:
cancelPreviousRequest(config)
逻辑:- 通过
AbortController
取消相同请求,防止前一次未完成的请求影响后续请求。
- 通过
3. 响应拦截器
在 response.interceptors.use()
中,我们:
- 处理不同返回状态:
- 如果
code === 1
,直接返回数据。 - 处理
Blob
或ArrayBuffer
类型(用于文件下载)。 - 针对特定
code
(如10105
、10106
代表 Token 失效):- 提示用户重新登录,并执行
userStore.resetToken()
进行重置。
- 提示用户重新登录,并执行
- 其他异常情况,则调用
ElMessage.error(msg)
进行提示。
- 如果
4. 处理请求异常
在 error
处理逻辑中:
- 如果是手动取消请求(
axios.isCancel(error)
),则阻止 Promise 继续执行。 - 如果是服务器返回错误:
- 读取
error.response.data
并根据code
进行不同处理。 10105
、10106
处理 Token 失效时,弹出ElMessageBox.confirm()
让用户重新登录。
- 读取
5. 请求取消管理
我们通过 controllerMap
:
- 利用
AbortController
追踪config.signal
,防止重复请求。 - 在相同
url + AbortController
标识下:- 取消前一个请求(
controllerMap.get(controllerKey)?.abort()
)。 - 重新创建
AbortController
并存储到controllerMap
。
- 取消前一个请求(
总结
我们的 axios
封装具备:
- 基础配置封装:默认
baseURL
、超时、Content-Type
处理。 - 请求拦截优化:并发控制、Token 处理、请求取消。
- 响应拦截优化:统一处理
code
,支持文件下载,错误提示优化。 - 请求异常管理:支持
AbortController
取消请求,Token 失效自动处理。
这样封装后:
- 减少了重复代码,提高了代码复用性;
- 提升了请求稳定性,减少了重复/无效请求;
- 增强了错误处理,提升了用户体验。
代码
1import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
2import qs from "qs";
3import { useUserStoreHook } from "@/store/modules/user";
4import { limitRequest } from "./limit-request";
5import { isJSON } from "./index";
6
7const headersReg = /(?<=application\/)text|json|xml|html|javascript|form-data/; //chrome62,62+,Firefox78+、Edge79+才支持后行断言
8// 创建 axios 实例
9const service = axios.create({
10 baseURL: import.meta.env.DEV
11 ? import.meta.env.VITE_APP_BASE_API
12 : import.meta.env.VITE_APP_API_URL,
13 // baseURL: import.meta.env.VITE_APP_BASE_API,
14 timeout: 50000,
15 headers: { "Content-Type": "application/json;charset=utf-8" },
16 transformRequest: [
17 function (data, headers) {
18 // Do whatever you want to transform the data
19 // raw格式
20 if (headers.hasOwnProperty("Content-Type") && headersReg.test(headers["Content-Type"])) {
21 return data;
22 } else {
23 //默认格式
24 //使用 qs.stringify() 序列化以后,调用接口,数据传输模式会自动修改为content-type: application/x-www-form-urlencoded
25 return qs.stringify(data);
26 }
27 },
28 ],
29 withCredentials: true,
30});
31
32// 请求拦截器
33service.interceptors.request.use(
34 (config: InternalAxiosRequestConfig) => {
35 cancelPreviousRequest(config);
36 const userStore = useUserStoreHook();
37 if (userStore.token) {
38 // config.headers.Authorization = userStore.token;
39 }
40 return new Promise((resolve) => {
41 // 并发请求控制
42 const promise = new Promise((_resolve) => {
43 Object.assign(config, { _resolve });
44 });
45
46 limitRequest.add({
47 promise,
48 request: () => {
49 resolve(config);
50 },
51 });
52 });
53 },
54 (error: any) => {
55 return Promise.reject(error);
56 }
57);
58
59// 响应拦截器
60service.interceptors.response.use(
61 (response: AxiosResponse) => {
62 response.config._resolve();
63 const { code, message: msg } = response.data;
64 if (code === 1) {
65 return response.data;
66 }
67 // 响应数据为二进制流处理(Excel导出)
68 if (response.data instanceof ArrayBuffer) {
69 return response;
70 }
71 if (response.data instanceof Blob) {
72 return response;
73 }
74 if (code === 10105 || code === 10106) {
75 // ElMessage.error(msg || "error");
76 const userStore = useUserStoreHook();
77 userStore.resetToken().then(() => {
78 location.reload();
79 });
80 } else if (code === 10204) {
81 ElMessage.error(msg || "系统异常");
82 } else if (code === 10205) {
83 ElMessage.error(msg || "请求超时");
84 } else if (code === 403) {
85 ElMessage.error(msg || "没有访问权限");
86 } else {
87 ElMessage.error(msg || "请求失败");
88 }
89 return Promise.reject(new Error(msg || "Error"));
90 },
91 (error: any) => {
92 error.config._resolve();
93 if (axios.isCancel(error)) {
94 // 中断promise链接
95 return new Promise(() => {});
96 }
97 if (error.response.data) {
98 const { code, msg } = error.response.data;
99 // token 过期,重新登录
100 if (code === 10105 || code === 10106) {
101 ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
102 confirmButtonText: "确定",
103 type: "warning",
104 }).then(() => {
105 const userStore = useUserStoreHook();
106 userStore.resetToken().then(() => {
107 location.reload();
108 });
109 });
110 } else {
111 ElMessage.error(msg || "系统出错");
112 }
113 }
114 return Promise.reject(error.message);
115 }
116);
117
118//controllerMap
119const controllerMap = new Map();
120//取消请求
121function cancelPreviousRequest(config) {
122 try {
123 const { url, signal, method, params, data } = config;
124 const parameter = method == "get" ? params : isJSON(data) ? JSON.parse(data) : data;
125 const controllerKey = url + (parameter?.AbortController ?? ""); //在url相同的情况下,AbortController的值可以作为唯一标识
126 if (parameter?.hasOwnProperty("AbortController")) {
127 //需要取消上一次请求的接口的标识参数
128 if (controllerMap.has(controllerKey) && signal?.aborted) {
129 //判断controllerMap的控制器signal是否被中止过
130 controllerMap.delete(controllerKey);
131 }
132 controllerMap.get(controllerKey)?.abort(); //中止上一个请求
133 const abortController = new AbortController(); //创建新的控制器对象
134 controllerMap.set(controllerKey, abortController); //存储控制器
135 config.signal = abortController.signal; //建立新的通信
136 }
137 } catch (err) {
138 console.log(err);
139 }
140}
141
142// 导出 axios 实例
143export default service;
⚙️
⚙️