03.04
前端判断数据类型
typeof
对象({})、数组([])、null 的结果都是 object
新数据类型如 Symbol、BigInt,无法识别
instanceof
无法判断基本数据类型和 null
Object.prototype.toString.call()
语法相对复杂,但是可以区分数组、函数等引用数据类型
Array.isArray()
只能判断数组类型,精准判断数组
typeof 和 instanceof 的区别:
返回值类型:typeof 返回一个表示数据类型的字符串,instanceof 返回一个布尔值,表示是否是指定类的实例
判断范围:typeof 相对来说可以判断的类型更多,instanceof 只能用来判断对象类型(也就是两者互补)。
精确性:typeof 对基本数据类型判断比较精确,对于引用类型则无法进一步区分。instanceof 可以准确的判断引用类型。
防抖和节流
防抖
防抖是指在用户事件被触发 n 秒后再执行回调逻辑,如果在这 n 秒内事件再次被触发,则重新计时。换言之,程序只执行最后一次触发事件,以此来优化性能。
实现代码:
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(args);
}, delay);
};
};
节流
节流是指当遇到连续用户事件时,以 n 秒为间隔进行阻断,目的是减少同一时间段内连续事件的触发频率,以此来提升性能。
实现代码:
const trottle = (fn, delay) => {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => {
fn(args);
clearTimeout(timer);
timer = null;
}, delay);
};
};
深拷贝
JSON.parse(JSON.stringify(obj))
缺点:JSON.stringify() 会丢失部分属性
关于 key 为 Symbol 的会忽略掉,值为 undefined 的会忽略掉,NaN 变成 null,函数会忽略掉,Infinity 会变成 null
总结来说:
无法复制函数和 undefined;
如果对象中包含循环引用,会抛出错误;
对于包含 Symbol、RegExp 等特殊类型的对象,可能无法正确工作
const obj = {
[Symbol.for('key1')]: 'a',
b: undefined,
c: NaN,
d: () => {
return null;
},
e: Infinity,
f: null,
};
console.log(JSON.parse(JSON.stringify(obj))); // {c: null, e: null, f: null}
![image-20240305135515117](/Users/fiat_lux/Library/Application Support/typora-user-images/image-20240305135515117.png)
使用 js 工具库 lodash
const obj = { a: 1, b: 2, c: 3 };
const newObj = _.cloneDeep(obj);
递归复制
function deepClone(obj) {
// 先判断 obj 的数据类型
if (obj === null) return obj; // obj 为 null
if (obj instanceof Date) return new Date(obj); // obj 为 Date
if (obj instanceof RegExp) return new RegExp(obj); // obj 为 RegExp
if (typeof obj !== 'object') return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
扩展:
JSON.stringify(value, replacer, space)
参数一:value 表示要被序列化的对象,接受对象或数组类型
参数二:replacer 用于标记需要序列化的属性,接受数组和函数类型
参数三:space 用于描述序列化后的缩进字符数,用于美化格式
使用场景:
- localStorage 的存储
localStorage 只能存储字符串类型,kv(key,value) 结构
const obj = { a: 1, b: 2, c: 3 };
// 序列化要存储的数据: JSON.stringify(obj)
// 存储
localStorage.setItem('obj', JSON.stringify(obj));
// 取出
localStorage.getItem('obj'); // '{"a":1,"b":2,"c":3}'
// 移除
localStorage.removeItem('obj');
对象的深拷贝
jsconst obj = { a: 1, b: 2, c: 3 }; const objA = obj; const objB = JSON.parse(JSON.stringify(obj)); // 修改 objA.a = 11; console.log(obj); // {a: 11, b: 2, c: 3} console.log(objA); // {a: 11, b: 2, c: 3} objB.a = 123; console.log(obj); // {a: 11, b: 2, c: 3} console.log(objB); // {a: 123, b: 2, c: 3}
删除对象属性
jsconst obj = { a: 1, b: 2, c: 3 }; const str = JSON.stringify(obj, (key, value) => { if (key === 'b') { return undefined; } return value; }); console.log(str); // '{"a":1,"c":3}' const objA = JSON.parse(str); console.log(objA); // {a: 1, c: 3}
前端跨域
跨域是前端解决的范畴
什么时候会发生跨域
跨域是由于浏览器的同源策略(协议、域名、端口)所导致的,是发生在 页面 到 服务端 请求的过程中
项目中怎么解决这个跨域问题的
Nginx 反向代理(可以使用 docker 开镜像)
例如:server 监听 5000 端口,如果访问路径是 / ,那么代理转发到 3000 端口,然后设置 header 相关属性。如果访问路径是/api,就转发到 4000 端口,同样设置 header 相关属性。这样,通过就解决跨域了。
jsserver { listen 5000; server_name 127.0.0.1; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /api { proxy_pass http://127.0.0.1:4000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
搭建 BFF 层
// 架设 BFF 层解决跨域问题
const KoaRouter = require('koa-router');
const router = new KoaRouter();
router.post('/api/task', async (ctx, next) => {
const res = await axios.post('http://127.0.0.1:4000/api/task');
ctx.body = res.data;
});
app.use(router.routes());
app.use(router.allowedMethods());
实际项目中使用什么方法解决还要看项目中使用的相关技术
尽量以最简单快捷的方法
了解后端处理跨域的原理吗
可以使用 nginx,但是我们项目后端是在微服务中处理跨域
cookie 和 localStorage 的区别
项目中的登录是用的 cookie 还是 localStorage 来保存 token 的?
cookie
用户在浏览器这边进行登录接口请求,服务端收到这个请求之后,在用户名和密码都正确的情况下,服务器在向浏览器返回登录结果的时候,会生成一个 cookie,并且在 Http Response Header 中 Set-Cookie。这样,当浏览器再次请求服务端时,都会同步的带上 cookie,cookie 会附带在每个 Http 请求上。
生成机制
服务端生成,在 Http Response Header 中 Set-Cookie (我们项目中就是使用的这种)
客户端生成,通过 document.cookie 设置
Cookie 设置初衷是用于维持 HTTP 状态,不用于存储数据。因此 cookie 有以下缺点:
- 大小限制:每个 cookie 项只能存储 4K 数据
- 性能浪费: cookie 附带在 http 请求上,数据量过大,会导致每个 http 请求就非常庞大,会很消耗流量和带宽。
前端和后端是同一个域名吗?
那肯定不是同一个
接口请求也是不一样的,所以跨域了
Web Storage - localStorage 和 session Storage
Web Storage:专注存储
Cookie:专注维持 HTTP 状态
LocalStorage
特点:以域名维度,浏览器的持久化存储方案,在域名下一直存在,及时关闭会话窗口或浏览器也不会被删除。
大小:每个项可以存储 5M 的数据
接口:同步接口,阻塞线程
使用方法:
// 存储
localStorage.setItem('obj', obj);
// 取出
localStorage.getItem('obj');
// 移除
localStorage.removeItem('obj');
SessionStorage
特点:以域名维度,浏览器基于会话级别的存储方案,它只有在当前会话窗口存储的数据才可以读取到,一旦关闭当前页面或新开一个窗口,之前存储的数据是获取不到的。
大小:每个项能存储 5M 的数据
接口:同步接口,阻塞线程
使用方法:
// 存储
sessionStorage.setItem('obj', obj);
// 取出
sessionStorage.getItem('obj');
// 移除
sessionStorage.removeItem('obj');
Vue3 diff 算法
快速 diff 算法
React
常用的一些 hooks
useState('初始值')
定义函数组件的状态
useEffect(fn, dependencies)
又称副作用 hooks。
作用:给没有生命周期的组件添加结束渲染的信号。
执行时机:在渲染结束之后执行。
useCallback(fn, dependencies)
是一个允许你在多次渲染中缓存函数的 React Hook
useMemo
useRef
useContxt
useEffect 和 useLayoutEffect 的区别?谁先执行?谁后执行?
https://blog.csdn.net/Likestarr/article/details/133863860
虚拟 DOM
最早是由 React 团队提出来的。
Virtual Dom 是一种编程概念。在这个概念里,UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中。
也就是说,只要我们有一种方式,能够将真实的 DOM 的层次结构描述出来,那么这就是一个虚拟 DOM。
在 React 中,React 团队使用的是 JS 对象来对 DOM 结构进行一个描述。
虚拟 DOM 和 JS 对象之间的关系:前者是一种思想,后者是一种思想的具体实现。
为什么需要虚拟 DOM
使用虚拟 DOM 主要有两个方面的优势:
- 相较于 DOM 的体积优势和速度优势
- 多平台的渲染抽象能力
相较于 DOM 的体积优势和速度优势
首先我们需要明确一点,JS 层面的计算速度要比 DOM 层面的计算更快;
- DOM 对象最终被浏览器渲染出来之前,浏览器会有很多工作要做(浏览器渲染原理)。
- DOM 对象上面的属性非常多
const div = document.createElement('div');
for (let i in div) {
console.log(i + ' ');
}
操作 JS 对象的时间和操作 DOM 对象的时间是完全不一样的。
JS 层面的计算速度要高于 DOM 层面的计算速度。
此时有一个问题:虽然使用了 JS 对象来描述 UI,但是最终不还是要用原生 DOM API 去操作 DOM 吗?
虚拟 DOM 在第一次渲染页面的时候,其实并没有什么优势,速度肯定比直接操作原生 DOM API 要慢一些,虚拟 DOM 真正体现优势是在更新阶段。
根据 React 团队的研究,在渲染页面时,相比使用原生 DOM API,开发人员更倾向于使用 innerHTML
let newP = document.createElement('p');
let newContent = document.createTextNode('this is a test');
newP.appendChild(newContent);
document.body.appendChild(newP);
document.body.innerHTML = `
<p>
this is a test
</p>
`;
因此在使用 innerHTML 的时候,就涉及到了两个层面的计算:
- JS 层面:解析字符串
- DOM 层面:创建对应的 DOM 节点
接下来我们加入虚拟 DOM 来进行对比:
innerHTML | 虚拟 DOM | |
---|---|---|
JS 层面计算 | 解析字符串 | 创建 JS 对象 |
DOM 层面计算 | 创建对应的 DOM 节点 | 创建对应的 DOM 节点 |
虚拟 DOM 真正发挥威力的时候,是在更新阶段
innerHTML 进行更新的时候,要全部重新赋值,这意味着之前创建的 DOM 节点需要全部销毁掉,然后重新进行创建
但是虚拟 DOM 只需要更新必要的 DOM 节点即可
innerHTML | 虚拟 DOM | |
---|---|---|
JS 层面计算 | 解析字符串 | 创建 JS 对象 |
DOM 层面计算 | 销毁原来所有的 DOM 节点 | 修改必要的 DOM 节点 |
DOM 层面计算 | 创建对应的 DOM 节点 |
多平台的渲染抽象能力
UI = f (state) 这个公式进一步拆分可以拆分成两步:
- 根据自变量的变化计算出 UI
- 根据 UI 变化执行具体的宿主环境的 API
虚拟 DOM 只是对真实 UI 的一个描述,根据不同的宿主环境,可以执行不同的渲染代码:
- 浏览器、Node.js 宿主环境使用 ReactDOM 包
- Native 宿主环境使用 ReactNative 包
- Canvas、SVG 或者 VML(IE8)宿主环境使用 ReactArt 包
- ReactTest 包用于渲染出 JS 对象,可以很方便的测试“不隶属于任何宿主环境的通用功能”
React 中的虚拟 DOM
在 React 中通过 JSX 来描述 UI,JSX 最终会被转为一个叫做 createElement 方法的调用,调用该方法后就会得到虚拟 DOM 对象。
经过 Babel 编译后结果如下:
![image-20240305220900674](/Users/fiat_lux/Library/Application Support/typora-user-images/image-20240305220900674.png)
在源码中 createElement
面试题:什么是虚拟 DOM?其优点有哪些?
react 使用 虚拟 dom 的好处?为什么会提高性能?
其实直接操作 dom 性能是最高的。
React diff 的原理
03.06
03.06 面试
对项目有进行哪些配置吗
环境配置:根据项目运行的环境,配置不同的环境变量。例如,开发环境、测试环境和生产环境可能需要请求不同的 API 地址、token 等。
依赖管理:使用包管理器(如 npm、Maven、Gradle 等)来管理项目的依赖。这些依赖可能包括库、框架、插件等。
代码规范:配置代码规范检查工具(如 ESLint、TSLint、Checkstyle 等),以确保代码风格一致、符合规范,并减少潜在的错误。
构建和部署:配置构建工具(如 Webpack、Gradle Build 等)来自动化构建过程,包括代码编译、打包、测试等。同时,配置部署工具(如 Docker、Kubernetes 等)以自动化部署应用到服务器。
安全性配置:配置项目的安全策略,如身份验证、授权、加密等,以确保项目的安全性。
React 里面常用的 Hooks
useState
:用于在函数组件中添加状态。它接受一个初始状态值,并返回一个包含当前状态和一个更新状态的函数的数组。useEffect
:用于在函数组件中执行副作用操作,例如数据获取、订阅或手动更改 React 组件的 DOM。它接受一个函数作为参数,并在组件渲染后执行该函数。此外,还可以指定一个依赖项数组,以便在依赖项发生变化时重新运行副作用函数。useContext
:用于在函数组件中访问 React 的 Context API。它接受一个 Context 对象作为参数,并返回该 Context 的当前值。这样,你可以在函数组件中使用 Context,而无需手动传递 props。useReducer
:用于在函数组件中管理复杂的状态逻辑。它接受一个 reducer 函数和一个初始状态值作为参数,并返回一个包含当前状态和一个更新状态的 dispatch 函数的数组。使用useReducer
可以更好地组织和管理状态更新逻辑,特别是在处理多个状态变量或执行异步操作时。useRef
:返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。这对于管理 DOM 对象、定时器或其他需要在组件生命周期内保持引用的值很有用。useMemo
:返回一个记忆化的值,该值只在依赖项数组发生变化时才会重新计算。这对于避免重复计算和提高性能很有用。useCallback
:返回一个记忆化的版本的回调函数,该回调函数载依赖项数组发生变化时才会更新。这对于防止不必要的渲染和提高性能很有用。
项目中使用的 redux
场景题:
前端页面会向后端去获取一些数据,用于列表展示,比如博客列表、菜谱列表,那数据量相对来说是比较大的,所以当接口请求数据较慢的时候,前端可以做哪些优化处理呢?
后端数据请求比较慢的时候,前端可以在体验上做一些优化,毕竟数据层面不是我们可以去掌控的。留大屏、loading、虚拟滚动、分页
当后端接口请求数据较慢时,前端可以采取以下几种优化处理策略:
- 懒加载(Lazy Loading): 对于非首屏显示的数据,如滚动列表中的项目,可以使用懒加载技术。这意味着在用户滚动到需要显示数据的区域之前,不会请求数据。这减少了初始加载时的请求量,提高了首屏加载速度。
- 分页(Pagination): 对于大量数据,不要一次性加载全部,而是将数据分为多个页面,用户可以按需加载。这减轻了服务器的压力,并降低了每次请求的数据量。
- 数据缓存(Caching): 对于不经常变更的数据,前端可以使用本地缓存技术(如 localStorage、sessionStorage 或 IndexedDB)来存储之前请求过的数据。当再次需要这些数据时,可以先检查本地缓存,如果数据存在且未过期,则直接使用缓存数据,避免重复请求后端。
- 请求合并(Request Batching): 如果多个请求可以合并为一个,那么应该尽量减少单独的请求次数。例如,当页面需要加载多个资源时,可以合并这些请求为一个请求。
- 请求优化(Request Optimization): 优化请求参数,减少不必要的字段或数据,确保请求的数据量尽可能小。此外,可以考虑使用 HTTP/2 或更高版本的协议,利用多路复用、头部压缩等技术来减少传输延迟。
- 加载动画与占位符(Loading Animations & Placeholders): 在数据加载过程中,为用户提供加载动画或占位符,使用户知道数据正在加载中,而不是让页面处于空白或停滞状态。
- 预加载(Preloading): 预加载是一种预测性加载技术,根据用户的操作习惯或页面上下文,提前加载用户可能需要的数据。例如,在滚动列表时,可以提前加载下一页的数据。
- 延迟加载(Delayed Loading): 对于一些非核心功能或用户可能不会立即注意到的数据,可以使用延迟加载。即先加载核心数据,然后等待一段时间(如用户滚动到页面底部)后再加载其他数据。
- 服务端渲染(Server-Side Rendering, SSR)或预渲染(Prerendering): 对于需要大量数据渲染的页面,可以考虑使用服务端渲染技术。这样,用户在首屏加载时就能看到完整的数据,而不是等待前端请求和渲染。预渲染则是一种在构建阶段生成静态 HTML 页面的技术,适用于内容不经常变更的场景。
- 优化后端接口性能: 前端优化只是提升用户体验的一部分,同时也需要关注后端接口的性能。确保后端接口响应迅速,数据处理高效,以及数据库查询优化等。
综合应用以上策略,可以在很大程度上提升用户在加载大量数据时的体验。
跨域是怎么解决的
跨域问题是指在一个域名下的网页去请求另一个域名下的资源时,由于浏览器的同源策略限制,导致请求被阻止的问题。解决跨域问题有多种方法,以下是一些常见的解决方案:
- JSONP(JSON with Padding):JSONP 利用了 script 标签不受同源策略限制的特性,通过在前端动态创建 script 标签,并设置其 src 属性为需要跨域请求的 URL,从而绕过同源策略限制。但 JSONP 只支持 GET 请求,且存在安全风险,因此使用时需要谨慎。
- CORS(Cross-Origin Resource Sharing):CORS 是一种基于 HTTP 头的跨域解决方案,它允许服务器指定哪些域可以访问其资源。通过在响应头中添加 Access-Control-Allow-Origin 标签,服务器可以允许指定域的请求访问其资源。CORS 支持多种 HTTP 请求方法,包括 GET、POST、PUT、DELETE 等。
- 代理:代理服务器可以作为一个中间人,将前端的请求先发送给代理服务器,再由代理服务器向目标服务器发起请求。由于代理服务器与目标服务器同源,因此可以绕过浏览器的同源策略限制。这种方法需要搭建代理服务器,并在前端配置请求地址为代理服务器的地址。
- WebSocket:WebSocket 是一种基于 TCP 的全双工通信协议,它不受同源策略限制。因此,可以通过 WebSocket 实现跨域通信。但需要注意的是,WebSocket 的连接建立过程仍然受到同源策略的限制,因此需要在建立连接前进行一些额外的配置。
在实际应用中,可以根据具体的需求和场景选择合适的解决方案。例如,对于简单的 GET 请求,可以使用 JSONP;对于需要支持多种 HTTP 请求方法的情况,可以使用 CORS;对于需要绕过浏览器限制的情况,可以考虑使用代理或 WebSocket。
类组件和函数组件的生命周期对比
https://vue3js.cn/interview/React/class_function component.html#三、区别
React 框架生命周期(类组件与函数组件):https://juejin.cn/post/6871728918643081230#heading-21
在 React 中,类组件和函数组件的生命周期存在显著的差异。
类组件的生命周期主要分为三个阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。在挂载阶段,组件首次被渲染到页面上,主要执行的方法包括 constructor、componentDidMount 等。在更新阶段,组件的状态或属性发生变化,导致组件重新渲染,主要执行的方法包括 componentDidUpdate 等。在卸载阶段,组件从页面上被移除,主要执行的方法包括 componentWillUnmount 等。
相比之下,函数组件在 React 16.8 版本之前并没有生命周期的概念,因为它们只是纯函数,没有实例状态,也不支持生命周期方法。然而,随着 React Hooks 的引入,函数组件也开始拥有了类似生命周期的功能。通过使用 useEffect Hook,函数组件可以在特定的时间点执行副作用操作,这些时间点类似于类组件的生命周期方法。例如,useEffect 可以在组件挂载后执行(类似于 componentDidMount),在组件更新后执行(类似于 componentDidUpdate),以及在组件卸载前执行(类似于 componentWillUnmount)。
需要注意的是,虽然函数组件通过 useEffect 可以模拟类组件的生命周期,但两者在实现方式和灵活性上仍有所不同。类组件的生命周期方法是在类的实例上调用的,因此可以通过 this 关键字访问组件的实例属性和方法。而函数组件则没有实例概念,它们通过参数接收属性和状态,并通过返回值来渲染界面。这种差异使得函数组件更加轻量级和易于复用,但同时也需要开发者更加熟悉 React Hooks 的使用方式。
如果想在页面中发送请求,类组件和函数组件分别写在哪个生命周期
比如想在页面加载的时候发送请求,应该写在哪个生命周期
类组件:
React 中请求通常在componentDidMount 生命周期函数中发送。 这个生命周期函数在组件已经挂载到页面上,并且可以操作 DOM 元素时被调用。
函数组件:
useEffect
useEffect 的参数有哪些?有哪几种写法
有两个参数,第一个参数是函数类型,第二个是数组
写法:
不传递第二个参数:会导致每次渲染都会执行 useEffect。当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发 useEffect,这就是问题所在。
jsuseEffect(() => { console.log(1); setNumber(num); });
第二个参数为空数组:在挂载和卸载时执行
jsuseEffect(() => { console.log(1); }, []);
第二个参数为数组,有一个或多个值:依赖值更新时执行
js// 依赖一个值 useEffect(() => { console.log(1); }, [val]); // 依赖多个值 useEffect(() => { console.log(1); }, [val, num]);
第一个函数参数中 return 一个方法:该方法在组件销毁的时候会被调用
jsuseEffect(() => { const timer = setInterval(() => { console.log(1); }, 1000); console.log(1); return () => { clearInterval(timer); }; }, []);
lodash 常用的方法有哪些
http://www.qyhever.com/pages/js/common-use-lodash-methods.html
antd 的图表在使用时,哪个图表给你带来了困扰?有问题,但是也解决了
upload 上传的时候,文件大小超过 5M 就上传失败,应该怎么处理
upload 上传组件的进度条样式和 UI 的不一样,现在是上下布局,希望改成左右布局,重构进度条部分的功能,应该怎么做?
让后端提供文件读取的百分比
博客分享链接给别人的时候,我希望别人点进来之后只能看到博客正文,其他的侧边栏之类的都看不到,应该怎么做?
隐藏样式
比如 url 多携带一个参数,用来判断是否需要隐藏元素
要在前端实现这样的功能,你可以通过结合 URL 参数和 JavaScript/CSS 来控制页面上元素的显示与隐藏。下面是一个详细的步骤说明:
步骤 1: 定义 URL 参数
首先,你需要为你的分享链接定义一个 URL 参数,比如?view=clean
,这样链接看起来可能是这样的:https://yourblog.com/your-post?view=clean
。
步骤 2: 读取 URL 参数
接下来,你需要在前端 JavaScript 代码中读取这个 URL 参数。你可以使用原生的 JavaScript 方法,也可以使用一些库(如 jQuery)来简化这个过程。以下是一个使用原生 JavaScript 读取 URL 参数的例子:
function getQueryParam(name) {
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(name);
}
const viewParam = getQueryParam('view');
步骤 3: 根据参数隐藏元素
现在,你可以根据读取到的参数值来决定是否隐藏某些元素。比如,如果viewParam
的值是clean
,你就隐藏侧边栏和页脚:
if (viewParam === 'clean') {
// 隐藏侧边栏
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.style.display = 'none';
}
// 隐藏页脚
const footer = document.getElementById('footer');
if (footer) {
footer.style.display = 'none';
}
// 隐藏其他任何你不想显示的元素...
}
步骤 4: 在适当的时候执行 JavaScript 代码
确保你的 JavaScript 代码在 DOM 加载完成后执行。你可以将上述代码放在window.onload
事件处理器中,或者使用DOMContentLoaded
事件,或者将<script>
标签放在 HTML 文档的底部。
document.addEventListener('DOMContentLoaded', function () {
// ...将上述代码放在这里...
});
步骤 5: 测试
最后,测试你的实现是否按预期工作。尝试访问带有?view=clean
参数的 URL,并确认侧边栏和其他不想要的元素确实被隐藏了。同时,也要确保在没有该参数的情况下,页面正常显示。
注意事项
- 安全性:这种方法仅依赖于前端技术,因此并不是完全安全的。用户仍然可以通过修改 URL 或禁用 JavaScript 来查看原本被隐藏的内容。
- 可维护性:如果你的博客模板经常变动,确保更新你的 JavaScript 代码以匹配最新的 DOM 结构。
- 性能:虽然这种方法对性能的影响通常很小,但最好还是尽量减少不必要的 DOM 操作和页面重绘。
- 用户体验:考虑分享页面的用户体验,确保即使在“清洁”视图下,用户也能轻松地导航回你的博客的其他部分或找到他们需要的信息。
数据处理、权限、图表、
上传文件请求函数
这段代码定义了一个名为 uploadProductFile
的函数,该函数用于上传产品文件。下面是对这段代码的详细解析:
函数参数
req
: 一个对象,其中至少包含onProgress
,onSuccess
, 和onError
三个方法,用于在上传过程中和上传完成后处理进度、成功和错误的情况。callback
: 一个回调函数,用于在上传完成后执行。
函数逻辑
- 触发进度事件:
req.onProgress({ percent: 10 });
当函数开始时,首先触发一个进度事件,表示上传开始,进度为 10%。
- 创建 FormData 对象:
var formData = new FormData();
formData.append('file', req.file);
使用 FormData
对象来准备要上传的数据。这里假设 req.file
是要上传的文件。
发送 AJAX 请求: 使用
$.ajax
发送一个 POST 请求到apiHost + "product/uploadExecl"
。请求头:
jsheaders: { Authorization: getUserToken(), }
在请求头中添加一个
Authorization
字段,其值通过调用getUserToken()
函数获取。内容类型和处理数据:
jscontentType: false, // 注意这里应设为false processData: false,
由于我们使用的是
FormData
对象,所以需要将contentType
和processData
都设置为false
,以确保文件能够被正确上传。自定义 XMLHttpRequest:
jsxhr: function () { var xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', function (e) { var progressRate = (e.loaded / e.total) * 100; req.onProgress({ percent: progressRate }) }) return xhr; }
这里自定义了
XMLHttpRequest
对象,并为上传过程添加了进度监听。每当上传进度更新时,都会调用req.onProgress
方法,并传递当前的进度百分比。成功和失败的处理:
js.done(function (data) { req.onProgress({ percent: 100 }); req.onSuccess(data); callback(data); }) .fail(function (data) { req.onProgress({ percent: 0 }); req.onError(data); callback(data); }) .fail(function (data) { req.onProgress({ percent: 0 }); req.onError(data); callback(data); });
使用
.done
方法处理上传成功的情况,使用.fail
方法处理上传失败的情况。注意这里.fail
方法被调用了两次,这可能是代码的重复,应该删除一个。
总结
这段代码定义了一个用于上传文件的函数,它使用 FormData
对象和 $.ajax
方法来发送文件,并在上传过程中和上传完成后通过回调函数通知调用者上传的进度和结果。但是,代码中存在一些可能的问题,如 .fail
方法的重复调用和没有处理可能的网络错误等。
//批量上传商品
export function uploadProductFile(req, callback) {
req.onProgress({ percent: 10 });
var formData = new FormData();
formData.append('file', req.file);
$.ajax({
url: apiHost + 'product/uploadExecl',
method: 'POST',
data: formData,
headers: {
Authorization: getUserToken(),
},
contentType: false, // 注意这里应设为false
processData: false,
cache: false,
xhr: function () {
//请求条
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function (e) {
var progressRate = (e.loaded / e.total) * 100;
req.onProgress({ percent: progressRate });
});
return xhr;
},
})
.done(function (data) {
req.onProgress({ percent: 100 });
req.onSuccess(data);
callback(data);
})
.fail(function (data) {
req.onProgress({ percent: 0 });
req.onError(data);
callback(data);
})
.fail(function (data) {
req.onProgress({ percent: 0 });
req.onError(data);
callback(data);
});
}
https://blog.csdn.net/Likestarr/article/details/133863860
https://blog.csdn.net/runrun117/article/details/124727815
3 月 4 号面试题
1. 前端判断数据类型的方法有哪些
JS 数据类型:
7 种基本数据类型:string、number、boolean、undefined、null、symbol、bigint,存储在栈中,存取方式是先进后出
引用数据类型:对象(object)、数组(array)、函数(function)存储在堆中,按优先级排序,优先级可以按照大小来规定。
- typeof
数组、对象、null 都会判断为 object,其他类型正确判断
- instanceof
只能正确判断引用数据类型,不能判断基本数据类型 instanceof 可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
- constructor
constructor 有两个作用:一是判断数据类型(可以判断全部类型),二是对象实例通过 constructor 对象访问它的构造函数。 需要注意的是,如果创建一个对象来改变它的原型,constructor 就不能用来判断数据类型了。
const Fn = function () {};
Fn.prototype = new Array();
const f = new Fn();
console.log(Fn.constructor === Function); // true
console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true
解析:f.constructor
也即f.__proto__.constructor
, 实例对象的__proto__
指向其构造函数的原型对象 prototype。 故f.__proto__
等价于 Fn.prototype
, 所以f.__proto__.constructor === Fn.prototype.constructor
Fn.prototype
是Array
的实例对象 实例对象的constructor
属性指向构造函数本身 所以Fn.prototype.constructor === Array
其实就是new Array()
得到的对象覆盖了原来的Fn.prototype
,所以Fn.prototype.constructor
不再指向Fn
,而是指向了Array
- Object.prototype.toString.call()
Object.prototype.toString.call() 是使用 Object 对象的原型方法 toString 来判断数据类型
扩展:同样是检测对象 obj 调用 toString 方法,obj.toString() 的结果和 Object.prototype.toString.call(obj)的结果不一样,这是为什么? 这是因为 toString 是 Object 的原型方法,而Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString()不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。
补充提问:typeof 和 instanceof 的区别
- 返回值类型不一样:typeof 返回一个表示数据类型的字符串,instanceof 返回一个布尔值,表示是否为指定类的实例。
- 判断范围和精确性不一样:typeof 对基本数据类型判断较精确,但是对引用类型无法进一步区分。对于对象、数组、null 会判断成 object。instanceof 判断引用类型更精确,无法判断基本数据类型。
补充提问:typeof 为什么判断 null 时会判断成 object(自己补充)
在 JS 第一个版本之后,所有值都存储在 32 位的单元中,每个单元包含一个小的**类型标签(1-3 bits)**以及当前要存储值的真实数据。类型标签存储在每个单位的低位中,共有 5 种数据类型:
000: object - 当前存储的数据指向一个对象
1: int - 当前存储的数据是一个 31 位的有符号整数
010: double - 当前存储的数据指向一个双精度的浮点数
100: string - 当前存储的数据指向一个字符串
110: boolean - 当前存储的数据是布尔值
如果最低位是 1,则类型标签标志位的长度只有 1 位;如果最低位是 0,则类型标签标志位的长度占 3 位,为存储其他四种数据类型提供了额外两个 bit 的长度。 有两种特殊数据类型:
- undefined 的值是(-2^30)(一个超出整数范围的数字)
- null 的值是机器码 NULL 指针(null 指针的值全是 0)
那么也就是说,null 的 类型标签也是 000,和 Object 的类型标签一样,所以会被判定为 Object。
2. 防抖和节流
防抖
防抖是指用户事件被触发 n 秒后执行回调逻辑,如果在这 n 秒内,事件再次被触发,则重新计时。换言之,程序只执行最后一次触发事件,一次来优化性能。
应用场景:
- 搜索框输入的时候,用户输入 1-9,不加防抖页面会渲染 9 次。加上防抖之后,页面渲染一次即可。
- 防止多次提交按钮,只执行最后一次提交
实现代码:
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer); // 先清除定时器
timer = setTimeout(() => {
// 然后设置定时器,执行回调逻辑
fn(args);
}, delay);
};
};
节流
节流是指当用户遇到连续用户事件时,以 n 秒为间隔进行阻断,目的是减少同一时间段内连续事件的触发频率,以此来提升性能。
应用场景:
- 拖拽场景:固定时间内执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器 resize(调整浏览器窗口大小,触发 resize 事件)
- 动画场景:避免短时间内多次触发动画引发性能问题
实现代码:
const trottle = (fn, delay) => {
let timer = null;
return function (...args) {
if (timer) return; // timer 存在,说明已经执行了回调函数,暂时不再执行
timer = setTimeout(() => {
// 先执行回调逻辑,再清除定时器
fn(args);
clearTimeout(timer);
timer = null;
}, delay);
};
};
3. 深拷贝
- JSON.parse(JSON.stringify(obj))
- 遇到 undefined 和函数会报错;
- 对象里面 key 为 Symbol、值为 undefined、值为函数,都会被忽略;
- 对象里面值为 NaN、值为 Infinity 会变成 null。
- js 工具库 lodash.cloneDeep(obj)
- 递归复制
- Object.assign({}, obj)
- 只深拷贝第一层
扩展: JSON.parse 和 JSON.stringify 还可以用于删除对象属性
4. 前端跨域
什么时候发生跨域
跨域是由于浏览器的同源策略所导致的,是发生在页面到服务端请求的过程中
怎么解决跨域
- Nginx 反向代理(可以使用 docker 开镜像)
server {
listen 5000;
server_name 127.0.0.1;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
- 搭建 BFF 层来解决跨域问题(Node 环境)
const KoaRouter = require('koa-router');
const router = new KoaROuter();
router.post('/api/task', async (ctx, next) => {
const res = await axios.post('http://127.0.0.1:4000/api/task');
ctx.body = res.data;
});
app.use(router.routes());
app.use(router.allowedMethos());
延伸问题:引入 BFF 层有什么好处
BFF 全称 Backend For Frontend
,一般是指在前端与服务器端搭建一层由前端维护的 Node Server 服务。 其好处有:
- 数据处理。对数据进行校验、清洗及格式化,使得数据与前端更契合。
- 数据聚合。后端无需处理大量的表连接工作,第三方接口聚合工作,业务逻辑简化为各个资源的增删改查,由 BFF 层聚合各个资源的数据,后端可集中处理性能问题、监控问题、消息队列等。
- 权限前移。在 BFF 层统一认证鉴权,后端无需做权限校验,后端可直接部署在集群内网,无需向外网暴露服务,减少了后端复杂度。 其坏处是:
- 引入复杂度。新的 BFF 服务需要一套基础设施的支持,如日志、异常、部署、监控等。
- 设置 CORS 头部字段
Access-Control-Allow-Origin: http://api.bob.com // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒
5. Cookie 和 localStorage
项目中的登录是用什么来存储 token 的
cookie
用户在浏览器输入账号密码,发送登录请求,服务端都到请求后,从数据库拿取数据来判断账号密码是否正确。在正确的情况下,服务器在向浏览器返回登录结果的时候,会生成一个 cookie,并且在 Http Response Header 中 Set-Cookie。这样,当浏览器再次向服务端发送请求时,都会同步的带上 cookie,cookie 会附带在每个 Http 请求上。
生成机制有两种:
服务端生成,在 Http Response Header 中 Set-Cookie(GoCart 项目中使用的)
客户端生成,通过 document.cookie 设置
cookie 和 Web Storage(localStorage、sessionStorage)的区别
Cookie 设置初衷是用于维持 HTTP 状态,不用于存储数据,因此 cookie 有一下缺点:
- 大小限制:每个 cookie 项只能存储 4K 数据
- 性能浪费:cookie 附带在 http 请求上,数据量过大,会导致每个 http 请求非常庞大,会很消耗流量和带宽。
Web Storage 专注存储
localStorage
- 特点:以域名维度,浏览器的持久化存储方案,在域名下一直存在,即使关闭会话窗口或浏览器,除非主动清理,否则永久存在;
- 大小:每个项可以存储 5M 的数据
- 接口:同步接口,会阻塞线程
sessionStorage
- 特点:以域名维度,浏览器基于会话级别的存储方案,它只有在当前会话窗口存储的数据才可以读取到,一旦关闭当前页面或新开一个窗口,之前存储的数据就获取不到了。刷新页面不会删除数据。
- 每个项可以存储 5M 的数据
- 接口:同步接口,阻塞线程
6. React 常用的 Hooks
- useState
状态是变化的数据,是组件甚至前端应用的核心。useState 有传入值和函数两种参数,返回的 setState 也有传入值和函数两种参数。
// 传入值
const [state, setState] = useState(0);
// 传入函数
const [num, setNum] = useState(() => {
let a = 1,
b = 2;
return a + b;
});
setState(1);
setNum((state) => state + 1); // 函数的参数是上一次的 state
- useEffect
副作用 effect 函数是在渲染之外额外执行的一些逻辑。它是根据第二个参数的依赖数组是否变化来决定是否执行 effect,可以返回一个清理函数,会在组件卸载前执行或每次使用更改的依赖性重新渲染之前运行。 执行时机:在渲染结束之后
useEffect(() => {
let timer = setTimeout(() => {
console.log(num);
}, 5000);
return () => {
// 清理函数
clearTimeout(timer);
};
}, [num]); // 如果不传第二个参数时,每次都会执行;传递第二个参数时,第二个参数有变化时执行
- useLayoutEffect
和 useEffect 差不多,但是 useEffect 的 effect 函数是异步执行的,所以可能中间有次渲染会闪屏。而 useLayoutEffect 是同步执行的,所以不会闪屏,但如果计算量大可能会导致掉帧,阻塞渲染。所以,仅当在浏览器渲染之前运行效果至关重要时才需要此功能,例如:在用户看到工具提示之前测量和定位工具提示。(只有在关键时刻需要在用户看到之前运行你的 Effect 时才需要使用它,例如,在显示提示工具提示之前测量和定位位置。)
- useInsertionEffect
回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。
- useReducer
封装一些修改状态的逻辑到 reducer,通过 action 触发。当修改深层对象的时候,创建新对象比较麻烦,可以结合 immer 来解决。
- useRef
可以保存 dom 引用或其他内容,通过.current
来取,改变它的内容不会触发重新渲染。
- forwardRef + useImperativeHandle
通过 forwardRef 可以从子组件转发 ref 到父组件。如果想自定义 ref 内容可以使用 useImperativeHandle 来实现。
- useContext
跨层组件之间传递数据可以使用 Context。用 createContext 创建 context 对象,用 Provider 修改其中的值,函数组件使用 useContext 来取值,类组件使用 Consumer 来取值。
- memo + useMemo + useCallback
memo 包裹的组件只有在 props 变化的时候才会重新渲染,useMemo、useCallback 可以防止 props 不必要的变化,两者一般是结合使用。不过当用来缓存计算结果等场景的时候,也可以单独使用 useMemo、useCallback。
补充提问:说一说 useEffect 和 useLayoutEffect 的区别
在 React 中,用于定义有副作用因变量的 Hook 有:
- useEffect:回调函数会在 commit 阶段完成后异步执行,所以不会阻塞视图渲染
- useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作
每一个 effect 会与当前 FC 其他的 effect 形成环状链表,连接方式为单向环状链表。 其中 useEffect 工作流程可以分为:
- 声明阶段
- 调度阶段
- 执行阶段
useLayoutEffect 的工作流程可以分为:
- 声明阶段
- 执行阶段
之所以 useEffect 会比 useLayoutEffect 多一个阶段,就是因为 useEffect 的回调函数会在 commit 阶段完成后异步执行,因此需要经历调度阶段。
7. 虚拟 DOM
虚拟 DOM 最早是由 React 团队提出来的,这是一种编程思想,指的是针对真实 UI DOM 的一种描述能力。 在 React 中,使用了 JS 对象来描述真实的 DOM 结构。虚拟 DOM 和 JS 对象之间的关系:前者是一种思想,后者是一种思想的具体实现。
虚拟 DOM 的优势
使用虚拟 DOM 主要有两个方面的优势:
- 相较于 DOM 的体积优势和速度优势
- 多平台渲染的抽象能力
1. 相较于 DOM 的体积优势和速度优势
- JS 层面的计算速度要比 DOM 层面的计算快得多
- DOM 对象最终被浏览器渲染出来之前,浏览器会有很多工作要做(浏览器渲染原理)
- DOM 对象上面的属性非常多
- 虚拟 DOM 发挥优势的时机主要体现在更新阶段,相比较 innerHTML 要将已有的 DOM 节点全部销毁,虚拟 DOM 能够做到针对 DOM 节点做最小程度的修改
2. 多平台渲染的抽象能力
- 浏览器、Node.js 宿主环境使用 ReactDOM 包
- Native 宿主环境使用 ReactNative 包
- Canvas、SVG 或者 VML(IE8)宿主环境使用 ReactArt 包
- ReactTest 包用于渲染出 JS 对象,可以很方便的测试“不隶属于任何宿主环境的通用功能”
在 React 中通过 JSX 来描述 UI,JSX 仅仅是一个语法糖,会被 Babel 编译为 createElement 方法的调用。该方法调用之后会返回一个 JS 对象,该对象就是虚拟 DOM 对象,官方更倾向于称之为一个 React 元素。
8. React diff 算法
diff 计算发生在更新阶段,当第一次渲染完成后,就会产生 Fiber 树,再次渲染的时候(更新),就会拿新的 JSX 对象(vdom)和旧的 FiberNode 节点进行一个对比,再决定如何来产生新的 FiberNode,它的目标是尽可能的复用已有的 Fiber 节点。这个就是 diff 算法。
在 React 中整个 diff 分为单节点 diff 和多节点 diff。 所谓单节点是指新的节点为单一节点,但是旧节点的数量是不一定的。 单节点 diff 是否能够复用遵循如下的顺序:
- 判断 key 是否相同
- 如果更新前后均未设置 key,则 key 均为 null,也属于相同的情况
- 如果 key 相同,进入步骤 2
- 如果 key 不同,则无需判断 type,结果为不能复用(有兄弟节点还会去遍历兄弟节点)
- 如果 key 相同,再判断 type 是否相同
- 如果 type 相同,那么就复用
- 如果 type 不同,则无法复用(并且兄弟节点也一并标记为删除)
多节点 diff 会分为两轮遍历: 第一轮遍历会从前往后进行遍历,存在以下三种情况:
- 如果新旧子节点的 key 和 type 都相同,说明可以复用
- 如果新旧子节点的 key 相同,但是 type 不同,这个时候就会根据 ReactElement 来生成一个全新的 fiber,旧的 fiber 被放入到 deletions 数组里面,回头统一删除。但是注意,此时遍历遍历并不会终止
- 如果新旧子节点的 key 和 type 都不相同,结束遍历
如果第一轮遍历被提前终止了,那么意味着还有新的 JSX 元素或者旧的 FiberNode 没有被遍历,因此会采用第二轮遍历去处理。 第二轮遍历会遇到三种情况:
- 只剩下旧子节点:将旧的子节点添加到 deletions 数组里面直接删除(删除的情况)
- 只剩下新的 JSX 元素:根据 ReactElement 元素来创建 FiberNode 节点(新增的情况)
- 新旧子节点都有剩余:会将剩余的 FiberNode 节点放入一个 map 里面,遍历剩余的新的 JSX 元素,然后从 map 中去寻找能够复用的 FiberNode 节点,如果能够找到,就拿来复用(移动的情况) 如果不能找到,就新增。然后如果剩余的 JSX 元素都遍历完了,map 结构中还有剩余的 Fiber 节点,就将这些 Fiber 节点添加到 deletions 数组里面,之后统一做删除操作。
整个 diff 算法最核心的就是两个字“复用”。
React 不使用双端 diff 的原因: 由于双端 diff 需要向前查找节点,但每个 FiberNode 节点上都没有反向指针,即前一个 FiberNode 通过 sibling 属性指向后一个 FiberNode,只能从前往后遍历,而不能反过来,因此该算法无法通过双端搜索来进行优化。 React 想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。React 认为对于列表反转和需要进行双端搜索的场景是少见的,所以在这一版的实现中,先不对 bad case 做额外的优化。
3 月 6 号面试题
1. 有对项目进行哪些配置吗
1. 环境配置
根据项目运行的环境,配置不同的环境变量。例如开发环境、测试环境和生产环境可能需要请求不同的 API 地址、token 等;
在 ops 项目中:
项目的开发、测试环境和生产环境所请求的 API 是不一致的。这个时候我们在 utils 文件中会配置 env 的值,以及其相对的 api 地址。切换不同环境时,修改 env 的值以确保使用正确的 api 地址。
let env = 'prod';
let apiHost = '';
let poHost = '';
if (env == 'test') {
apiHost = 'https://testapi.grosiraja.com/cms/';
poHost = 'https://testapi.grosiraja.com/supply/';
}
if (env == 'dev') {
apiHost = 'https://devapi.grosiraja.com/cms/';
poHost = 'https://devapi.grosiraja.com/supply/';
}
if (env == 'prod') {
apiHost = 'https://api.grosiraja.com/cms/';
poHost = 'https://api.grosiraja.com/supply/';
}
export { apiHost, poHost };
web 和 wap 项目也是如此
let env = 'gro';
let ipUrl = '';
let prodIpUrl = '';
if (env == 'gro') {
ipUrl = 'https://api.gor.grosiraja.com'; // gro
prodIpUrl = 'https://api.gor.grosiraja.com';
}
if (env == 'dev') {
ipUrl = 'https://dev-api.gocart.ph'; // dev
prodIpUrl = 'http://10.60.13.197:8000';
}
if (env == 'staging') {
ipUrl = 'https://staging-api.gocart.ph'; //staging
prodIpUrl = 'http://10.60.13.198:8000'; // prod http
}
if (env == 'master') {
ipUrl = 'https://pre-prod-api.gocart.ph'; // prod https
prodIpUrl = 'http://10.60.14.51:8000'; // prod http
}
2. 依赖环境
使用包管理器(如 npm、Maven、Gradle 等)来管理项目的依赖。这些依赖可能包括库、框架、插件等。
3. 代码规范
配置代码规范检查工具(如 ESLint、TSLint、Checkstyle 等),以确保代码风格一致、符合规范,并减少潜在的错误。
比如统一代码缩进 2 个或 4 个字符。修改 vscode 的 Tab Size 的值
4. 构建和部署
配置构建工具(如 webpack、gradle builddeng)来自动化构建过程,包括代码编译、打包、测试等。同时,配置部署工具(如 Docker、Kubernetesdeng)以自动化部署应用到服务器。
比如 ops 项目: 这是一个已有的项目,主要是进行项目维护
使用的是 umi 框架搭建
- 因为项目代码量大,所以会遇到打包速度慢的情况 如果遇到编译慢、增量编译慢、内存爆、OOM 等问题,可以尝试以下方法。
所以进行在 config 目录下的 config.js 文件里配置了
nodeModulesTransform: {
type: 'none',
exclude: [],
}
这也是官方提供的一个解决方案
- 当遇到打包占用内存太大,电脑可用内存不足,打包失败
我们进行打包的时候,项目过大时,可能会遇到(webpack)打包过程中占用的内存堆栈超出了 node.js 中采用 V8 引擎对内存的限制,V8 引擎对内存的使用的默认大小限制是 1536 (1.5 GiB),那我们就可以通过修改这个内存限制来解决。
所以对 package.json 文件的 build 命令进行一个修改:
// 修改前
"build": "umi build",
// 修改后
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 umi build"
5. 安全性配置
配置项目的安全策略,如身份验证、授权、加密等,以确保项目的安全性
比如 ops
对于请求数据有进行 RSA 非对称加密,使用的是 jsencrypt 库
var encryptor = new JSEncrypt(); // 创建加密对象实例
//之前ssl生成的公钥,复制的时候要小心不要有空格
var pubKey =
'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1QQRl0HlrVv6kGqhgonD6A9SU6ZJpnEN+Q0blT/ue6Ndt97WRfxtSAs0QoquTreaDtfC4RRX4o+CU6BTuHLUm+eSvxZS9TzbwoYZq7ObbQAZAY+SYDgAA5PHf1wNN20dGMFFgVS/y0ZWvv1UNa2laEz0I8Vmr5ZlzIn88GkmSiQIDAQAB-----END PUBLIC KEY-----';
encryptor.setPublicKey(pubKey); //设置公钥
var rsaPassWord = encryptor.encrypt('要加密的内容'); // 对内容进行加密
ops 的实现
公钥会根据 env 的值来进行调整,不同的环境使用不同的公钥
SEncrypt.prototype.encryptLong = function (string) {
var k = this.getKey();
try {
var ct = '';
//RSA每次加密117bytes,需要辅助方法判断字符串截取位置
//1.获取字符串截取点
var bytes = new Array();
bytes.push(0);
var byteNo = 0;
var len, c;
len = string.length;
var temp = 0;
for (var i = 0; i < len; i++) {
c = string.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10ffff) {
//特殊字符,如Ř,Ţ
byteNo += 4;
} else if (c >= 0x000800 && c <= 0x00ffff) {
//中文以及标点符号
byteNo += 3;
} else if (c >= 0x000080 && c <= 0x0007ff) {
//特殊字符,如È,Ò
byteNo += 2;
} else {
// 英文以及标点符号
byteNo += 1;
}
if (byteNo % 117 >= 114 || byteNo % 117 == 0) {
if (byteNo - temp >= 114) {
bytes.push(i);
temp = byteNo;
}
}
}
//2.截取字符串并分段加密
if (bytes.length > 1) {
for (var i = 0; i < bytes.length - 1; i++) {
var str;
if (i == 0) {
str = string.substring(0, bytes[i + 1] + 1);
} else {
str = string.substring(bytes[i] + 1, bytes[i + 1] + 1);
}
var t1 = k.encrypt(str);
ct += t1;
}
if (bytes[bytes.length - 1] != string.length - 1) {
var lastStr = string.substring(bytes[bytes.length - 1] + 1);
ct += k.encrypt(lastStr);
}
return ct;
}
var t = k.encrypt(string);
// var y = hex2b64(t);
return t;
} catch (ex) {
return false;
}
};
const encrypt = new JSEncrypt();
if (env == 'test') {
encrypt.setPublicKey(
'-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz5sQUPqadstAXXEbXe+kKaRyxvuAQFUXLRzncCFG5Z5nPEuIW5UqnquUW5H08lkg+HMm7qipDRKveU3nWLV325sYib7CRAGMt1knsoOYi6d6OX9yvb27Fxm/lzDnJD0+pRUZKIz+sq4cC5fTovR9kqY3AO6wYUoAZw7xmYCMnMQF43Nimrn01/P8JNj9YgdaYZq6Jn6KUqjF8kRM8+Q2HdcwXoL9WMPILqbf+fa3V3tyXK2DMarLOLvmOmRLS9lIIvTZ6tH09OoTKl6Pg6FMyd5uvfphF407CM5CsSW3pNckw8Aernp9O3+s6Wi4FnxbrzSfhYCPlm0k01VEmzsdPwIDAQAB-----END PUBLIC KEY-----'
);
}
if (env == 'dev') {
encrypt.setPublicKey(
'-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnDk/VvHj+rB83SL3vYxFasr7FQLu7ZvPhQhCv5GJM0JoQeTQ+Uyxs3QZZD6roOiXl74hcr/2WZz5wH5UzPTQ4gi/xdmDqfBVZNiPyMoCwQag+BZ3hKoasV6csy6GGf9hxu4oM/qNlcH3a4+cRh8JoGIYRmBDHVNBcUz6g/q6HB9jd4DptFCdGYHLA+fi2tyZby/+kf5UXuevVt+65CRcSsvfF88MpdDx+QTkC879UZ16B0H++0uB98LShIH1Kt1kSESaG9c3bUp3o0vJHbyK05Bu/UfspD0qdk8JsJkI2uxiVH6l3NHfmQor5oMLQAcnJZDcrbtQxYiymh9pfr1DeQIDAQAB-----END PUBLIC KEY-----'
);
}
if (env == 'prod') {
encrypt.setPublicKey(
'-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkQEICxoRgjffbzsXAGhvdlR4D+h+cuqwNyFeVKi9ZVJqLiq+qKTIsvwwopqMvoJ/+gHu0Qyq11Di7bl7COvrr0BLecunJsqEfhc45A7aUvb+mwHRh4/5pgRVci2TvUQEkYonkfe9eKaogESSHt7ET1RsJLA2nUGapZqcVsOLfqqf3556oi4BpqmmvOgR1Pz3DZ9jm46gVoormASNkizwkkfvnKjaEt1hFVQtus5yfKzPL7eSBzwl7naAW9l6e+sN9IFGHpOWZoQhXzG6vRipL5uvYoyvh3oHmqShZdVTnuNG1CbQTNB+9BiElUqX8kE+VOEK3cazden3MzP+t6u/FwIDAQAB-----END PUBLIC KEY-----'
);
}
2. React 里面常用的 Hooks
- useState
状态是变化的数据,是组件甚至前端应用的核心。useState 有传入值和函数两种参数,返回的 setState 也有传入值和函数两种参数。
// 传入值
const [state, setState] = useState(0);
// 传入函数
const [num, setNum] = useState(() => {
let a = 1,
b = 2;
return a + b;
});
setState(1);
setNum((state) => state + 1); // 函数的参数是上一次的 state
- useEffect
副作用 effect 函数是在渲染之外额外执行的一些逻辑。它是根据第二个参数的依赖数组是否变化来决定是否执行 effect,可以返回一个清理函数,会在组件卸载前执行或每次使用更改的依赖性重新渲染之前运行。 执行时机:在渲染结束之后
useEffect(() => {
let timer = setTimeout(() => {
console.log(num);
}, 5000);
return () => {
// 清理函数
clearTimeout(timer);
};
}, [num]); // 如果不传第二个参数时,每次都会执行;传递第二个参数时,第二个参数有变化时执行
- useLayoutEffect
和 useEffect 差不多,但是 useEffect 的 effect 函数是异步执行的,所以可能中间有次渲染会闪屏。而 useLayoutEffect 是同步执行的,所以不会闪屏,但如果计算量大可能会导致掉帧,阻塞渲染。所以,仅当在浏览器渲染之前运行效果至关重要时才需要此功能,例如:在用户看到工具提示之前测量和定位工具提示。(只有在关键时刻需要在用户看到之前运行你的 Effect 时才需要使用它,例如,在显示提示工具提示之前测量和定位位置。)
- useInsertionEffect
回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。
- useReducer
封装一些修改状态的逻辑到 reducer,通过 action 触发。当修改深层对象的时候,创建新对象比较麻烦,可以结合 immer 来解决。
- useRef
可以保存 dom 引用或其他内容,通过.current
来取,改变它的内容不会触发重新渲染。
- forwardRef + useImperativeHandle
通过 forwardRef 可以从子组件转发 ref 到父组件。如果想自定义 ref 内容可以使用 useImperativeHandle 来实现。
- useContext
跨层组件之间传递数据可以使用 Context。用 createContext 创建 context 对象,用 Provider 修改其中的值,函数组件使用 useContext 来取值,类组件使用 Consumer 来取值。
- memo + useMemo + useCallback
memo 包裹的组件只有在 props 变化的时候才会重新渲染,useMemo、useCallback 可以防止 props 不必要的变化,两者一般是结合使用。不过当用来缓存计算结果等场景的时候,也可以单独使用 useMemo、useCallback。
补充提问:说一说 useEffect 和 useLayoutEffect 的区别
在 React 中,用于定义有副作用因变量的 Hook 有:
- useEffect:回调函数会在 commit 阶段完成后异步执行,所以不会阻塞视图渲染
- useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作
每一个 effect 会与当前 FC 其他的 effect 形成环状链表,连接方式为单向环状链表。 其中 useEffect 工作流程可以分为:
- 声明阶段
- 调度阶段
- 执行阶段
useLayoutEffect 的工作流程可以分为:
- 声明阶段
- 执行阶段
之所以 useEffect 会比 useLayoutEffect 多一个阶段,就是因为 useEffect 的回调函数会在 commit 阶段完成后异步执行,因此需要经历调度阶段。
补充提问:项目中有没有用到 redux
3. 场景题 - 页面优化
前端页面会向后端去获取一些数据,用于列表展示,比如博客列表、菜谱列表,那数据量相对来说是比较大的,所以当接口请求数据较慢的时候,前端可以做哪些优化处理呢?
后端数据请求比较慢的时候,前端可以在体验上做一些优化,毕竟数据层面不是我们可以去掌控的。比如使用 Skeleton 骨架屏、loading、虚拟滚动、分页等。
- 懒加载 对于非首屏显示的数据,如滚动列表中的项目,可以使用懒加载。这意味着在用户滚动到需要显示数据的区域之前,不会请求数据。这减少了初始加载时的请求量,提高了首屏加载速度。
虚拟滚动
- 按需渲染:在同一时间内,只渲染我们看得见(视口)的 DOM 节点,这样浏览器需要渲染的节点就少了,从而降低渲染时长。
- 模拟滚动:在用户滚动滑轮或滑动屏幕的时候,模拟滚动行为去滚动列表,即根据滚动的位置重新渲染可见的列表元素。
- 分页
对于大量数据,不要一次性加载全部,而是将数据分为多个页面,用户可以按需加载。这减轻了服务器的压力,并降低了每次请求的数据量。
- 数据缓存
对于不经常变更的数据,前端可以使用本地缓存技术(如 localStroage、sessionStorage 或 IndexedDB)来存储之前请求过的数据。当再次需要这些数据时,可以先检查本地缓存,如果数据存在且未过期,则直接使用缓存数据,避免重复请求后端。
- 请求合并
如果多个请求可以合并为一个,那么应该尽量减少单独的请求次数。例如,当页面需要加载多个资源时,可以合并这些请求为一个请求。
- 请求优化
优化请求参数,减少不必要的字段或数据,确保请求的数据量尽可能小。此外,可以考虑使用 HTTP/2 或更高版本的协议,利用多路复用、头部压缩等技术来减少传输延迟。
- 加载动画与占位符
在数据加载过程中,为用户提供加载动画或占位符,使用户知道数据正在加载中,而不是让页面处于空白或停滞状态。
- 预加载
预加载是一种预测性加载技术,根据用户的操作习惯或页面上下文,提前加载用户可能需要的数据。例如,在滚动列表时,可以提前加载下一页的数据。
- 延迟加载
对于非核心功能或用户可能不会立即注意到的数据,可以使用延迟加载。即先加载核心数据,然后等待一段时间(如用户滚动到页面底部)后再加载其他数据。
9.服务端渲染或预渲染
对于需要大量数据渲染的页面,可以考虑使用服务端渲染技术。这样,用户在首屏加载时就能看到完整的数据,而不是等待前端请求和渲染。
预渲染则是一种在构建阶段生成静态 HTML 页面的技术,适用于内容不经常变更的场景。
- 优化后端接口性能
前端优化只是提升用户体验的一部分,同时也需要关注后端接口的性能。确保后端接口响应迅速,数据处理高效,以及数据库查询优化等。
跨域是怎么解决
跨域是由于浏览器的同源策略所导致,是发生在页面到服务端请求的过程中。
跨域解决方法:
如果想在页面加载的时候发送请求,类组件和函数组件分别写在哪里
类组件:
React 中请求通常在 componentDidMount 生命周期函数中发送。这个生命周期函数在组件已经挂载到页面上,并且可以操作 DOM 元素时调用。
函数组件:
在 useEffect 中发送请求
用过 lodash 吗?常用的方法有些?
用过。
常用的方法有:
自我介绍
面试官,你好,我叫刘艳兰。 21 年毕业于湖南工学院,软件工程专业,有 3 年前端工作经验,之前在四只悟空公司工作。技术栈主要是 React 生态相关。开发的项目主要是 gocart 购物网站平台,是包括 web、wap 端和 cms、oms 的,前台网站使用 Nextjs 框架搭建,后台使用 Umi 框架搭建。目前项目已经成功上线使用。ops 和一套关联的公司内部管理系统,运营系统,包括权限管理、订单管理、商品管理等运营相关内容。在过去的团队中,主要承担的任务是进行项目需求的开发和维护工作。
1. 介绍一下项目
2. 前端权限怎么做,token 的作用
添加一个 src/access.js 文件为我们的权限定义文件,在 routes.js 文件里面添加一些权限字段(比如:access 字段、authority 字段)
cms 中使用 umi max 的 useAccess 和 Access
使用 Access 包裹元素,使用 useAccess 获取权限相关信息
比如 access.canAuthority、 access.canBuAdmin 为 true 或 false
3. js 事件循环机制
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,其他线程只需要在合适的时候将任务加入队列末尾即可。
过去讲消息队列简单的分为宏队列和微队列,这种说法已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在一个队列,不同的任务可以属于不同的队列。不同的任务队列有不同的优先级。在一次事件循环中,由浏览器自行决定,取哪个队列的任务执行。但是浏览器必须有一个微队列,微队列的任务的优先级是最高的,必须优先调度执行。
常见的微任务有: Promise.then MutationObserver Object.observer(目前已废弃,Proxy 对象替代) process.nextTick (nodejs)
常见的宏任务: script(可以理解为外层同步代码) setTimeout / setInterval UI rendering / UI 事件 postMessage、MessageChannel setImmediate、I/O(输入输出事件)(nodejs)
内存泄露
内存泄露是指由于疏忽或错误造成程序没有及时释放已经不再使用的内存。对于持续运行的服务进程,必须及时释放不再用到的内存,否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
自动内存管理,减轻程序员的负担,这被称为“垃圾回收机制”
不再需要的内存 - 垃圾 无法触达的内存 - 回收 不再需要但还能触达的内存 - 内存泄露 无法触达但也回收不了的内存 - 内存泄露
闭包会不会导致内存泄露?会 为什么? 因为:
持有了不再需要的函数引用,会导致函数关联的词法环境无法销毁,从而导致内存泄露。
当多个函数共享词法环境时,会导致词法环境膨胀,从而导致出现无法触达也无法回收的内存空间,从而导致内存泄露。
垃圾回收机制
JS 具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存
原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存
通常情况下有两种实现方式:
标记清除(主要使用)
引用计数(效率高,有缺陷)
标记清除
当变量进入执行环境,就标记这个变量为“进入环境”。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境”。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文的变量引用的变量的标记去掉。
在此之后再被加上标记的变量就是待删除的了,原因是任何上下文中的变量都访问不到它们了。
随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存。
var m = 0,
n = 19; // 把 m、n、add() 标记为进入环境
add(m, n); // 把 a、b、c 标记进入环境
console.log(n); // a、b、c 标记为离开环境,等待垃圾回收
function add(a, b) {
a++;
var c = a + b;
return c;
}
引用计数
语言引擎有一张“引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是 0,就表示这个值不再用到了,因此可以将这块内存释放。
如果一个值不再需要了,引用数却不为 0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
const arr = [1, 2, 3];
console.log('hello world');
上面代码中,数组[1, 2, 3]是一个值,会占用内存。变量 arr 是仅有的对这个值的引用,因此引用次数为 1。尽管后面的代码没有用到 arr,它还是会持续占用内存。
如果需要这块被垃圾回收机制释放,只需要设置如下:
arr = null;
通过设置 arr 为 null,就解除了对数组[1, 2, 3]的引用,引用次数为 0,就被垃圾回收了。
常见的内存泄漏情况
- 意外的全局变量
function foo(tag) {
bar = 'hello';
}
另一种意外的全局变量可能有 this 创建
function foo() {
this.val = 'hello';
}
// foo 调用自己,this 指向了全局对象(window)
foo();
以上使用严格模式,可以避免意外的全局变量
- 定时器也可能造成内存泄漏
var someResource = getData();
setInterval(function () {
var node = document.getElementById('Node');
if (node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource);
}
}, 1000);
如果 id 为 Node 的元素从 DOM 中移除,定时器仍会存在。同时,因为回调函数中对 someResource 的引用,定时器外面的 someResource 也不会被释放。
包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放。
function bindEvent() {
var obj = document.createElement('xxx');
var unused = function () {
console.log(obj, '闭包内引用obj,obj不会被释放');
};
obj = null; // 解决办法
}
没有清理对 DOM 元素的引用同样造成内存泄漏
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom 删除了
console.log(refA, 'refA'); // 但是还存在引用,能输出整个 div,没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
包括使用事件监听 addEventListener 监听的时候,在不监听的情况下使用 removeEventListener 取消事件监听。
4. 你对闭包的理解
一个函数对其周围状态(词法环境)的引用捆绑,这样的组合就是闭包。
也就是说,闭包可以让你在一个内层函数中访问到其外层函数的作用域。
在 js 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。
function init() {
var name = 'joy'; // name 是一个被 init 函数创建的局部变量
function displayName() {
// displayName 函数是内部函数,一个闭包
alert(name); // 使用了 init 函数中声明的变量(函数外部变量)
}
displayName();
}
init();
displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量。
5. react 渲染流程,怎么实现可中断渲染 fiber 架构
6. useEffect 依赖项不同的区别
7. css 3 动画属性有哪些?怎么在 js 中实现动画
transition
在 js 中实现动画 requestAnimationFrame 在下一次重绘之前调用指定的回调函数更新动画
cancelAnimationFrame 取消动画