2024.11.06 - 2024.11.23 更新前端面试问题总结(20道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…
目录:
初级开发者相关问题【共计 2 道题】
- [React] useMemo 是否可以支持异步函数【热度: 410】【web框架】
- [React] useCallback 是否支持异步函数【热度: 410】【web框架】
中级开发者相关问题【共计 8 道题】
- 如果特别多的事件都挂载到一个元素上,会存在什么问题【热度: 336】【JavaScript】
- 绑定事件的元素节点销毁又重新创建, 绑定的事件还会生效吗【热度: 337】【JavaScript】
- 比如我把事件委托注册在 body 上面, 我如何去针对性的出发 不同的子元素【热度: 338】【JavaScript】
- 如果一行文本展示不下,如何使其通过 popover 来展示全部内容?【热度: 325】【JavaScript】【出题公司: PDD】
- 在 JS 里面, proxy set 拦截器, 有那些参数, 分别表示什么含义【热度: 120】【JavaScript】【出题公司: 美团】
- proxy set 拦截器,其中参数中第一个参数 target 和 最后一个参数 receiver 有何区别【热度: 132】【JavaScript】【出题公司: 美团】
- proxy 可以拦截数组变化吗【热度: 133】【JavaScript】【出题公司: 京东】
- corejs 是做什么用的, 跟 Polyfill 是什么关系【热度: 100】【工程化】
高级开发者相关问题【共计 10 道题】
-
CDN 是如何决策资源请求的【热度: 300】【网络】
-
Object.defineProperty 是否可以监听拦截数组变化【热度: 144】【JavaScript】【出题公司: 小米】
-
项目部署更新之后,如何提醒用户去刷新更新页面资源【热度: 340】【web应用场景】【出题公司: 百度】
-
Webpack 与 Vite的核心差异点在哪儿【热度: 610】【工程化】
-
Webpack 与 Vite 在产物结果上有何区别【热度: 420】【工程化】
-
[Webpack] 我如何实现在高版本浏览器上使用 es6 产物, 在低版本上使用 es5【热度: 122】【工程化】
-
[webpack] 在编译产物的时候, 要区分 source 代码和外部依赖代码, 该如何处理【热度: 125】【工程化】
-
[webpack] externals 是如何加载外部依赖的【热度: 330】【工程化】
-
vite 和 rollup 是什么样的依赖关系【热度: 153】【工程化】
-
Rollup 为何高效【热度: 200】【工程化】
初级开发者相关问题【共计 2 道题】
1068. [React] useMemo 是否可以支持异步函数【热度: 410】【web框架】
useMemo本身不直接支持异步函数,但可以通过一些方式来结合异步操作使用。
-
useMemo的基本原理和同步操作特性useMemo是一个 React Hook,用于优化组件的性能。它会在组件渲染时,根据依赖项数组中的值是否发生变化来决定是否重新计算记忆化的值。- 例如,在一个简单的同步场景中:
const memoizedValue = useMemo(() => { return expensiveComputation(a, b); }, [a, b]);这里,
expensiveComputation是一个同步函数,useMemo会在a和b的值没有改变的情况下,复用之前计算得到的memoizedValue,避免不必要的重复计算,从而提高性能。 -
结合异步函数的方式
- 使用
Promise和async/await在useEffect中触发异步操作并更新状态:- 可以先在
useEffect中触发异步操作,当异步操作完成后,通过setState更新组件状态,进而触发重新渲染。然后在需要使用异步计算结果的地方,可以使用useMemo来记忆化这个状态。 - 例如:
const [asyncResult, setAsyncResult] = useState(null); useEffect(() => { const fetchData = async () => { const result = await someAsyncFunction(); setAsyncResult(result); }; fetchData(); }, []); const memoizedAsyncResult = useMemo(() => asyncResult, [asyncResult]); - 可以先在
- 自定义
useAsyncMemo钩子(高级用法):- 如果需要在多个地方重复使用这种异步记忆化的逻辑,可以创建一个自定义的
useAsyncMemo钩子。这个钩子内部可以管理异步操作的状态(如加载中、错误、成功结果),并返回记忆化后的异步结果。 - 示例代码如下:
这样,在组件中就可以使用function useAsyncMemo(asyncFunction, dependencies) { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { setLoading(true); try { const res = await asyncFunction(); setResult(res); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchData(); }, dependencies); return [result, loading, error]; }const [memoizedAsyncResult, loading, error]=useAsyncMemo(someAsyncFunction, [dependency1, dependency2]);来获取异步记忆化的结果以及加载状态和错误信息。 - 如果需要在多个地方重复使用这种异步记忆化的逻辑,可以创建一个自定义的
- 使用
-
注意事项
-
避免在
useMemo内部直接使用异步函数进行计算,因为useMemo是同步执行的,它期望返回一个立即可用的值。如果在useMemo内部返回一个Promise,它会被当作普通对象处理,而不是等待异步操作完成后返回正确的值。 -
当结合
useEffect和useMemo来处理异步操作时,要注意useEffect的依赖项数组的设置,避免无限循环和不必要的重新渲染。同时,对于useAsyncMemo这样的自定义钩子,也要仔细考虑其内部状态管理和依赖项的处理,以确保正确的功能和性能。
-
1069. [React] useCallback 是否支持异步函数【热度: 410】【web框架】
-
useCallback的基本原理和同步特性useCallback是一个 React Hook,主要用于优化组件的性能。它返回一个记忆化的回调函数,这个函数只有在依赖项数组中的元素发生变化时才会重新创建。- 例如,在一个典型的同步场景下:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);这里的
doSomething是一个同步函数,useCallback会根据a和b的值是否改变来决定是否重新创建memoizedCallback。其目的是避免在组件重新渲染时不必要地重新创建相同的回调函数,从而减少潜在的性能开销,比如避免子组件因为接收的回调函数引用变化而重新渲染。 -
结合异步函数的方式
- 在回调函数内部调用异步函数:
useCallback本身可以包裹一个包含异步操作的回调函数。例如,可以在useCallback返回的函数内部调用async/await函数:
const memoizedAsyncCallback = useCallback(async () => { await someAsyncFunction(); // 其他操作 }, []);这样,
memoizedAsyncCallback是一个异步的记忆化回调函数,只有在依赖项数组(这里为空)中的元素发生变化时才会重新创建。当这个回调函数被调用时,它会执行异步操作。- 传递给子组件并在子组件中触发异步操作:可以将这个包含异步操作的记忆化回调函数传递给子组件,让子组件在合适的时机(比如用户交互或者组件生命周期的某个阶段)触发异步操作。例如:
function ParentComponent() { const memoizedAsyncCallback = useCallback(async () => { await someAsyncFunction(); // 其他操作 }, []); return <ChildComponent onAsyncAction={memoizedAsyncCallback} />; } function ChildComponent({ onAsyncAction }) { const handleClick = () => { onAsyncAction(); }; return <button onClick={handleClick}>触发异步操作</button>; } - 在回调函数内部调用异步函数:
-
注意事项
-
当使用
useCallback包裹异步回调函数时,要注意依赖项数组的设置。如果异步函数内部引用了外部变量,这些变量应该被正确地包含在依赖项数组中,否则可能会导致闭包问题或者使用过期的变量值。 -
虽然
useCallback可以处理包含异步操作的回调函数,但它并不能改变异步函数本身的执行性质。也就是说,异步函数仍然是异步执行的,useCallback只是控制了回调函数的创建频率。在处理异步操作的结果(如更新状态)时,需要遵循 React 的异步状态更新原则,比如在useEffect或者useState的更新函数中正确地处理异步返回的数据。
-
中级开发者相关问题【共计 8 道题】
1058. 如果特别多的事件都挂载到一个元素上,会存在什么问题【热度: 336】【JavaScript】
关键词:事件委托应用场景
如果将特别多的事件都挂载到一个元素上,比如在事件委托时将事件都绑定在 body 上,可能会存在以下问题:
一、性能问题
-
事件处理开销增加:
- 每次触发事件时,浏览器需要遍历所有绑定在该元素上的事件处理程序,确定哪个处理程序应该响应特定的事件。随着事件数量的增加,这个遍历过程会变得更加耗时,导致性能下降。
- 例如,当用户在页面上进行频繁的交互操作时,如果有大量事件绑定在
body上,浏览器可能会花费更多的时间来确定应该执行哪个事件处理程序,从而使页面的响应速度变慢。
-
内存占用增加:
- 每个事件处理程序都需要占用一定的内存空间。当有大量事件处理程序绑定到一个元素上时,会消耗更多的内存。这可能会对浏览器的性能产生负面影响,尤其是在内存受限的设备上。
- 如果页面中同时有其他复杂的操作或大量的数据处理,内存占用过高可能导致浏览器变得不稳定,甚至出现崩溃的情况。
二、可维护性问题
-
代码复杂性增加:
- 大量的事件处理程序集中在一个元素上,会使代码变得更加复杂和难以理解。这增加了开发和维护的难度,尤其是当需要修改或调试代码时。
- 例如,在查找特定事件处理程序的问题时,可能需要在大量的代码中进行搜索,这会耗费更多的时间和精力。
-
冲突和意外行为的风险增加:
- 当有多个事件处理程序同时响应一个事件时,可能会出现冲突或意外的行为。例如,一个事件处理程序可能会修改页面的状态,而另一个事件处理程序可能依赖于原始状态,导致不可预测的结果。
- 随着事件数量的增加,这种冲突的可能性也会增加,使得代码的调试和修复变得更加困难。
三、可扩展性问题
-
难以扩展和修改:
- 如果后续需要添加新的功能或修改现有功能,大量的事件处理程序绑定在一个元素上可能会使这个过程变得复杂。新的事件处理程序可能需要与现有的处理程序协调工作,这可能需要对现有代码进行大量的修改。
- 例如,当需要添加一个新的交互元素或改变页面的布局时,可能需要重新考虑事件的绑定方式,以确保新的元素能够正确地响应事件。
-
不利于模块化开发:
-
将大量事件集中在一个元素上不利于模块化开发。模块化开发强调将功能分解为独立的模块,每个模块负责特定的任务。如果事件处理程序都绑定在一个元素上,就难以将相关的功能分离出来,形成独立的模块。
-
这会使代码的可重用性降低,不利于团队协作和代码的维护。
-
1059. 绑定事件的元素节点销毁又重新创建, 绑定的事件还会生效吗【热度: 337】【JavaScript】
关键词:事件委托应用场景
比如我将事件绑定在 body 上面, 后来这个 body 人为移除掉了, 然后又重新创建了一个 body 标签, 那么之前绑定在 body 上面的事件还会生效吗?
如果将事件绑定在 body 上,然后移除了这个 body 标签并重新创建一个 body 标签,之前绑定在旧 body 上的事件不会生效在新的 body 上。
原因如下:
当你使用传统的事件绑定方式(如 addEventListener)将事件绑定到一个特定的 DOM 元素上时,这个绑定是针对特定的实例。一旦该元素被移除,与之相关的事件处理程序也会与该元素一起被销毁。当重新创建一个新的 body 标签时,它是一个全新的 DOM 元素,没有与之前被移除的 body 上的事件处理程序相关联。
例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
document.body.addEventListener("click", function () {
console.log("body clicked");
});
// 假设这里有一些代码移除了 body 并重新创建一个新的 body
const oldBody = document.body;
oldBody.parentNode.removeChild(oldBody);
const newBody = document.createElement("body");
document.documentElement.appendChild(newBody);
</script>
</body>
</html>
在这个例子中,点击新创建的 body 不会触发之前绑定的点击事件处理程序。
1060. 比如我把事件委托注册在 body 上面, 我如何去针对性的出发 不同的子元素【热度: 338】【JavaScript】
关键词:事件委托应用场景
这个问题属于一个典型的「事件委托」的应用场景
如果知识背诵八股文的同学, 可能这个问题就尴尬了
当把事件委托注册在 body 上时,可以通过以下方法针对性地触发不同子元素的特定行为:
一、利用事件对象的属性判断目标元素
-
event.target属性:- 当事件在
body上触发时,可以通过event.target来获取实际触发事件的元素。 - 例如:
document.body.addEventListener("click", function (event) { const target = event.target; if (target.classList.contains("button1")) { // 处理按钮 1 的点击事件 } else if (target.classList.contains("button2")) { // 处理按钮 2 的点击事件 } }); - 在这个例子中,通过检查
event.target的classList来确定点击的是哪个特定的按钮,然后执行相应的处理逻辑。
- 当事件在
-
matches()方法:- 可以使用
event.target.matches(selector)方法来检查目标元素是否与特定的 CSS 选择器匹配。 - 例如:
document.body.addEventListener("click", function (event) { const target = event.target; if (target.matches("#element1")) { // 处理元素 1 的点击事件 } else if (target.matches(".class2")) { // 处理具有特定类名的元素的点击事件 } }); - 这里使用
matches()方法来判断点击的元素是否与特定的 ID 或类名选择器匹配,从而执行相应的操作。
- 可以使用
二、使用数据属性进行区分
- 设置
data-*属性:- 可以在 HTML 元素上设置自定义的
data-*属性来标识不同的元素,并在事件处理函数中根据这些属性进行区分。 - 例如:
<button data-action="action1">Button 1</button> <button data-action="action2">Button 2</button> - 然后在 JavaScript 中:
document.body.addEventListener("click", function (event) { const target = event.target; if (target.dataset.action === "action1") { // 处理按钮 1 的点击事件 } else if (target.dataset.action === "action2") { // 处理按钮 2 的点击事件 } }); - 在这个例子中,通过检查元素的
data-action属性的值来确定执行哪个特定的操作。
- 可以在 HTML 元素上设置自定义的
通过这些方法,可以在事件委托到 body 的情况下,有针对性地处理不同子元素的事件,提高代码的效率和可维护性。
1062. 如果一行文本展示不下,如何使其通过 popover 来展示全部内容?【热度: 325】【JavaScript】【出题公司: PDD】
关键词:动态计算文本是否溢出
作者备注
主要考核 JS 动态计算文本是否溢出
以下是一种使用 HTML、CSS 和 JavaScript 来实现当文本一行展示不下时通过popover展示全部内容的基本方法。假设你在一个网页环境中操作。
-
HTML 结构
- 首先,创建一个包含文本的元素,例如一个
span标签。为这个元素添加一个自定义属性(比如data-full-text)来存储完整的文本内容。
<span id="textElement" data-full-text="这是一段很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的文本" > 这是一段很长很长很长的文本 </span> - 首先,创建一个包含文本的元素,例如一个
-
CSS 样式
- 为
span元素设置样式,使其在一行内显示文本,并在文本溢出时隐藏溢出部分。
#textElement { white - space: nowrap; overflow: hidden; text - overflow: ellipsis; cursor: pointer; } - 为
- 这里设置
cursor: pointer是为了让用户知道这个元素是可以点击的,当文本溢出时可以触发popover显示完整内容。
- JavaScript 功能实现
- 使用 JavaScript 来检测文本是否溢出。可以通过比较元素的
offsetWidth和scrollWidth来实现。如果scrollWidth大于offsetWidth,说明文本溢出了。 - 当文本溢出时,创建一个
popover来显示完整内容。可以使用一些现成的 JavaScript 库(如 Bootstrap 的popover插件)或者自己编写简单的popover功能。以下是一个使用自定义 JavaScript 实现简单popover功能的示例(不依赖第三方库):
document.addEventListener("DOMContentLoaded", function () { const textElement = document.getElementById("textElement"); if (textElement.scrollWidth > textElement.offsetWidth) { textElement.addEventListener("click", function () { const fullText = this.getAttribute("data-full-text"); const popover = document.createElement("div"); popover.className = "popover"; popover.textContent = fullText; document.body.appendChild(popover); // 简单的定位,将popover放在被点击元素的下方 const rect = this.getBoundingClientRect(); popover.style.left = rect.left + "px"; popover.style.top = rect.bottom + 5 + "px"; }); } });- 同时,你还需要添加一些 CSS 样式来美化
popover:
.popover { position: absolute; background - color: white; border: 1px solid gray; padding: 10px; border - radius: 5px; z - index: 100; } - 使用 JavaScript 来检测文本是否溢出。可以通过比较元素的
上述代码首先检查文本是否溢出。如果溢出,当用户点击该文本元素时,会创建一个popover元素并将完整文本内容放入其中,然后将popover添加到文档中,并简单地定位在被点击元素的下方。
请注意,这只是一个简单的示例,在实际应用中,你可能需要根据具体的设计要求和项目框架(如使用 Vue.js、React.js 等)来进行更复杂的实现,并且可能需要考虑浏览器兼容性等问题。如果使用像 Bootstrap 这样的框架,实现popover功能会更加方便和具有更好的样式一致性。
1063. 在 JS 里面, proxy set 拦截器, 有那些参数, 分别表示什么含义【热度: 120】【JavaScript】【出题公司: 美团】
关键词:proxy set 拦截器
target参数- 含义:它是被代理的目标对象。这个对象是原始的、即将被操作(在
set操作的情境下是被设置属性值)的对象。例如,如果你创建了一个代理来拦截对某个对象属性的设置操作,target就是那个实际拥有属性的原始对象。 - 示例:
在这个例子中,let targetObj = { name: "John" }; let proxyObj = new Proxy(targetObj, { set(target, property, value) { // 这里的target就是targetObj console.log(`正在设置${target}对象的${property}属性为${value}`); target[property] = value; return true; }, }); proxyObj.age = 30;target在proxyObj.age = 30这个操作中,就是targetObj,它是被代理的基础对象,set拦截器可以对这个对象的属性设置操作进行监控和修改。
- 含义:它是被代理的目标对象。这个对象是原始的、即将被操作(在
property参数- 含义:它表示要设置的属性名。在对象操作中,当你通过
obj[key] = value或者obj.property = value这样的方式设置属性时,property就是那个key或者property。这个参数让拦截器知道具体是哪个属性正在被操作。 - 示例:
在这里,当尝试设置let targetObj = { name: "John" }; let proxyObj = new Proxy(targetObj, { set(target, property, value) { if (property === "age" && typeof value !== "number") { throw new Error("年龄必须是数字"); } target[property] = value; return true; }, }); proxyObj.age = "abc";proxyObj.age时,property的值就是'age',拦截器可以根据这个属性名来进行特定的验证(如这里检查年龄是否为数字)。
- 含义:它表示要设置的属性名。在对象操作中,当你通过
value参数- 含义:它是要设置给属性的新值。在
obj[key]=value或者obj.property = value这样的操作中,value就是等号右边的值。拦截器可以获取这个值来决定是否允许设置,或者对这个值进行转换等操作。 - 示例:
在这个例子中,当设置let targetObj = { name: "John" }; let proxyObj = new Proxy(targetObj, { set(target, property, value) { if (typeof value === "string") { value = value.toUpperCase(); } target[property] = value; return true; }, }); proxyObj.name = "jane"; console.log(targetObj.name);proxyObj.name时,value最初是'jane',拦截器将其转换为大写'JANE'后再设置到targetObj的name属性中,最后targetObj.name的值为'JANE'。
- 含义:它是要设置给属性的新值。在
receiver参数(可选)-
含义:它通常是操作发生的对象。在大多数简单的情况下,它和
proxy对象本身(即被用来设置属性的代理对象)是相同的。不过,在一些复杂的继承或者代理链场景下,它可以帮助确定真正接收操作的对象。这个参数提供了一种机制来正确地处理属性访问的上下文。 -
示例:
let targetObj = { name: "John" }; let handler = { set(target, property, value, receiver) { console.log(`接收者是${receiver}`); target[property] = value; return true; }, }; let proxyObj = new Proxy(targetObj, handler); let anotherObj = Object.create(proxyObj); anotherObj.age = 30;在这个例子中,当通过
anotherObj(它继承自proxyObj)来设置属性age时,receiver参数将指向anotherObj,这可以帮助拦截器更好地理解操作的上下文。
-
1064. proxy set 拦截器,其中参数中第一个参数 target 和 最后一个参数 receiver 有何区别【热度: 132】【JavaScript】【出题公司: 美团】
关键词:proxy set 拦截器
-
target参数- 本质和用途
target是被代理的原始对象。它代表了代理操作所基于的实际对象。在Proxy的set拦截器中,target的主要作用是提供对原始对象属性和状态的访问,以便在拦截属性设置操作时,可以正确地将新值应用到原始对象的相应属性上。
- 示例说明
- 假设我们有一个简单的对象
originalObject = { value: 10 };,并创建了一个代理const proxy = new Proxy(originalObject, { set });。当拦截器set被触发时,target就是originalObject。例如:
const originalObject = { value: 10 }; const handler = { set(target, property, value) { console.log(`原始对象是: ${JSON.stringify(target)}`); target[property] = value; return true; }, }; const proxy = new Proxy(originalObject, handler); proxy.value = 20;- 在这个例子中,当设置
proxy.value时,target就是originalObject,set拦截器可以通过target来修改originalObject的value属性。
- 假设我们有一个简单的对象
- 本质和用途
-
receiver参数- 本质和用途
receiver是实际接收属性设置操作的对象。在简单的情况下,它通常就是代理对象本身。但是,在一些更复杂的场景中,比如涉及到对象的继承或者多层代理时,receiver和target可能不同,它能帮助确定操作发生的真实上下文。
- 示例说明
- 考虑这样一个场景,有一个基础对象
baseObject,创建了一个代理proxy1,然后又有一个对象通过Object.create(proxy1)创建并继承自proxy1。当在这个继承对象上进行属性设置操作时,receiver将指向这个继承对象,而target仍然是原始被代理的对象。
const baseObject = { count: 0 }; const handler = { set(target, property, value, receiver) { console.log(`接收操作的对象是: ${JSON.stringify(receiver)}`); console.log(`原始对象是: ${JSON.stringify(target)}`); target[property] = value; return true; }, }; const proxy1 = new Proxy(baseObject, handler); const derivedObject = Object.create(proxy1); derivedObject.count = 1;-
在这个例子中,当设置
derivedObject.count时,receiver是derivedObject,因为它是实际接收操作的对象,而target是baseObject,因为它是原始被代理的对象。这就体现了receiver和target在复杂场景下的区别。
- 考虑这样一个场景,有一个基础对象
- 本质和用途
1065. proxy 可以拦截数组变化吗【热度: 133】【JavaScript】【出题公司: 京东】
关键词:proxy set 拦截器
- 可以拦截数组变化
Proxy可以有效地拦截数组的变化。当对数组进行各种操作,如修改元素、添加或删除元素等,Proxy都能够捕获这些操作并进行拦截。
- 拦截数组的读取和设置操作
- 对于数组元素的读取和设置操作,可以通过
get和set拦截器来实现。 get拦截器示例:- 假设我们要拦截对数组元素的读取操作,以记录哪些元素被访问了。
let array = [1, 2, 3]; let proxyArray = new Proxy(array, { get(target, property, receiver) { console.log(`正在读取数组元素${property}`); return target[property]; }, }); let element = proxyArray[1];- 在这个例子中,当通过
proxyArray[1]读取数组元素时,get拦截器会被触发。它会先打印出正在读取数组元素1,然后返回数组中索引为1的元素(即2)。
set拦截器示例:- 假如我们想要拦截对数组元素的设置操作,比如限制数组元素的取值范围。
let array = [1, 2, 3]; let proxyArray = new Proxy(array, { set(target, property, value, receiver) { if (value < 0) { throw new Error("数组元素不能小于0"); } target[property] = value; return true; }, }); proxyArray[0] = -1;- 这里,当尝试将
proxyArray[0]设置为-1时,set拦截器会被触发。由于-1小于0,会抛出一个错误数组元素不能小于0。
- 对于数组元素的读取和设置操作,可以通过
- 拦截数组的方法调用
- 数组有许多方法,如
push、pop、shift、unshift、splice等。可以通过Proxy的apply拦截器来拦截这些方法的调用。 apply拦截器示例:- 假设我们要记录数组的
push方法的调用情况。
let array = [1, 2, 3]; let proxyArray = new Proxy(array, { apply(target, thisArg, argumentsList) { if (target.push === argumentsList[0]) { console.log("正在调用数组的push方法"); } return target.apply(thisArg, argumentsList); }, }); proxyArray.push(4);-
在这个例子中,当调用
proxyArray.push(4)时,apply拦截器会被触发。它会先打印出正在调用数组的push方法,然后执行正常的push操作,将4添加到数组中。
- 假设我们要记录数组的
- 数组有许多方法,如
1073. corejs 是做什么用的, 跟 Polyfill 是什么关系【热度: 100】【工程化】
关键词:corejs 和 Polyfill
-
CoreJs 的作用
- 提供标准的 JavaScript 功能支持:CoreJs 是一个 JavaScript 标准库,它提供了许多在现代 JavaScript 环境中被认为是标准的功能。这些功能包括但不限于对 ES6 + 语法的支持,如
Promise、Symbol、Map、Set等。例如,在一些较旧的浏览器中可能没有原生的Promise支持,CoreJs 可以提供一个兼容的Promise实现,使得代码能够在这些浏览器中正确运行。 - 跨浏览器兼容性支持:它能够填补不同浏览器之间的 JavaScript 功能差距。由于不同浏览器对 JavaScript 标准的实现进度不同,CoreJs 可以确保在各种浏览器环境下都能提供一致的功能。比如,某些浏览器可能没有完全实现
Array.prototype.includes方法,CoreJs 可以添加这个方法的实现,使得应用程序在这些浏览器中也能使用该功能。 - 模块化的功能提供:CoreJs 是模块化的,这意味着可以根据具体的需求选择引入特定的模块。例如,如果只需要在项目中添加
Promise和Map的支持,而不需要其他功能,可以只引入 CoreJs 相关的模块,避免不必要的代码体积增加。
- 提供标准的 JavaScript 功能支持:CoreJs 是一个 JavaScript 标准库,它提供了许多在现代 JavaScript 环境中被认为是标准的功能。这些功能包括但不限于对 ES6 + 语法的支持,如
-
CoreJs 与 Polyfill 的关系
-
CoreJs 是 Polyfill 的一种实现方式:Polyfill 是一个术语,用于描述一段代码,它提供了浏览器中缺失的功能。CoreJs 可以看作是一种全面的 Polyfill 库。当浏览器不支持某个 JavaScript 特性时,CoreJs 可以通过添加相应的代码来模拟该特性,就像填充了浏览器功能的空缺一样。例如,对于
Object.assign方法,如果浏览器不支持,CoreJs 可以提供一个自定义的函数来实现相同的功能,这个自定义函数就是一种 Polyfill。 -
Polyfill 可以有多种来源,CoreJs 是常用的一种:除了 CoreJs 之外,开发人员也可以自己编写 Polyfill 或者使用其他库来提供类似的功能。但是 CoreJs 是经过广泛测试和优化的,它涵盖了大量的 JavaScript 特性,并且能够很好地与 Babel 等工具配合使用。例如,在 Babel 的
@babel/preset - env配置中,使用useBuiltIns: 'usage'选项时,Babel 会根据目标浏览器的情况,自动引入 CoreJs 中的相关 Polyfill,以确保代码在目标浏览器中能够正确运行。这样就能够高效地为项目添加必要的 Polyfill,而不是无差别地引入所有可能的 Polyfill,从而减小代码体积。
-
高级开发者相关问题【共计 10 道题】
1057. CDN 是如何决策资源请求的【热度: 300】【网络】
关键词:CDN 资源请求决策
CDN(Content Delivery Network,内容分发网络)通过以下方式决策资源请求:
一、用户请求导向
-
DNS 解析引导:
- 当用户在浏览器中输入一个网址请求资源时,首先会进行 DNS(Domain Name System,域名系统)查询,将域名解析为对应的 IP 地址。
- CDN 利用 DNS 系统,将用户的请求导向到离用户最近的 CDN 节点。例如,用户在北京访问一个使用了 CDN 的网站,DNS 服务器会根据用户的地理位置和网络状况,返回一个位于北京或附近的 CDN 节点的 IP 地址。
- 这个过程通常是通过修改域名的 DNS 记录来实现的,将域名指向 CDN 提供商的 DNS 服务器,由 CDN 的 DNS 系统进行智能解析和调度。
-
Anycast 技术辅助:
- 一些 CDN 提供商还会使用 Anycast 技术,将同一个 IP 地址分配给多个位于不同地理位置的 CDN 节点。
- 当用户请求这个 IP 地址时,网络会自动将请求路由到离用户最近的节点。例如,一个 CDN 节点的 IP 地址为 192.168.1.1,这个 IP 地址同时被分配给了位于北京、上海、广州等地的节点。当用户在北京请求这个 IP 地址时,网络会自动将请求路由到北京的节点。
二、节点选择策略
-
地理位置就近原则:
- CDN 会根据用户的地理位置,选择距离用户最近的节点来响应请求。这样可以减少网络延迟,提高用户访问速度。例如,用户在上海,CDN 会优先选择上海或周边地区的节点来提供服务。
- 通过测量用户与各个节点之间的网络延迟、响应时间等指标,确定最近的节点。可以使用 traceroute 等工具来测量网络路径和延迟。
-
网络状况评估:
- CDN 会实时监测各个节点的网络状况,包括带宽利用率、丢包率、延迟等。
- 当用户请求资源时,CDN 会选择网络状况较好的节点来响应请求。例如,如果某个节点的带宽利用率过高或出现网络故障,CDN 会自动将请求导向到其他节点。
- 使用网络监测工具和算法,不断收集和分析节点的网络性能数据,以便做出最优的节点选择决策。
-
负载均衡考虑:
- CDN 会考虑各个节点的负载情况,避免某个节点负载过高而影响服务质量。
- 当用户请求资源时,CDN 会选择负载较轻的节点来响应请求。例如,如果某个节点正在处理大量的请求,CDN 会将新的请求分配到其他负载较轻的节点。
- 通过实时监测节点的负载指标,如并发连接数、处理请求的速度等,并使用负载均衡算法来分配请求。
三、资源缓存与更新策略
-
缓存机制:
- CDN 节点会缓存网站的静态资源,如图片、CSS 文件、JavaScript 文件等。
- 当用户请求这些资源时,CDN 节点可以直接从缓存中返回资源,而不需要从源服务器获取,从而大大提高响应速度。
- 根据资源的类型、大小、访问频率等因素设置缓存策略,确定资源的缓存时间和更新方式。
-
缓存更新:
- 当源服务器上的资源发生变化时,CDN 需要及时更新缓存。
- CDN 可以通过多种方式实现缓存更新,如主动推送、被动拉取等。主动推送是指源服务器在资源发生变化时,主动通知 CDN 节点更新缓存;被动拉取是指 CDN 节点在发现用户请求的资源与缓存中的资源不一致时,自动从源服务器获取最新的资源。
- 使用缓存更新策略可以确保用户始终获取到最新的资源,同时又能充分利用缓存提高访问速度。
四、性能优化与安全保障
-
性能优化措施:
- CDN 可以采用多种性能优化技术,如压缩资源、优化图片大小、合并文件等,减少资源的传输大小和加载时间。
- 还可以使用 HTTP/2、QUIC 等新的网络协议,提高网络传输效率。例如,使用 HTTP/2 的多路复用功能,可以在一个连接上同时传输多个请求和响应,减少连接建立的开销。
-
安全保障机制:
- CDN 可以提供一定的安全保障,如 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击防护、WAF(Web Application Firewall,Web 应用防火墙)等。
- 通过分布式的架构和强大的网络带宽,可以抵御大规模的 DDoS 攻击。WAF 可以检测和过滤恶意的网络请求,保护网站的安全。
- 确保用户请求的资源是安全可靠的,同时保护源服务器免受攻击。
补充知识:GSLB 在 CDN 资源请求决策中的作用
-
全局负载均衡
- GSLB 负责在全球范围内对 CDN 节点进行负载均衡。它通过收集各个 CDN 节点的状态信息,如负载、网络延迟、可用性等,来决定将用户请求导向哪个节点。
- GSLB 可以根据不同的策略进行决策,例如:
- 基于地理位置:将用户请求导向距离最近的节点。
- 基于网络性能:选择网络延迟最低、带宽最大的节点。
- 基于负载情况:避免将请求导向负载过高的节点。
-
智能 DNS 解析
- GSLB 通常与智能 DNS 系统结合使用。当用户进行 DNS 解析时,GSLB 会根据用户的 IP 地址和其他因素,返回一个最合适的 CDN 节点的 IP 地址。
- 这样,用户的请求就会被直接导向到选定的 CDN 节点,无需经过多次跳转,提高了访问速度和效率。
-
故障转移与高可用性
-
GSLB 还可以实现故障转移和高可用性。如果某个 CDN 节点出现故障或不可用,GSLB 可以快速将用户请求导向其他正常的节点,确保服务的连续性。
-
它会不断监测各个节点的状态,及时发现并处理故障,提高整个 CDN 系统的可靠性。
-
1066. Object.defineProperty 是否可以监听拦截数组变化【热度: 144】【JavaScript】【出题公司: 小米】
关键词:Object.defineProperty 监听数组变化
-
基本原理与部分可行性
Object.defineProperty可以用于监听和拦截数组的某些变化,但不是原生地对所有数组操作都能很好地监听。- 数组在 JavaScript 中是特殊的对象,其索引可以看作是对象属性。理论上,我们可以使用
Object.defineProperty为数组的每个索引(属性)定义属性描述符,以此来尝试监听数组元素的读取和设置操作。 - 例如,对于一个简单的数组元素设置操作,可以这样定义:
let arr = [1, 2, 3]; Object.defineProperty(arr, "0", { get: function () { console.log("读取索引为0的元素"); return arr[0]; }, set: function (value) { console.log("设置索引为0的元素"); arr[0] = value; }, });- 当通过
arr[0]读取或设置元素时,相应的get和set函数会被触发,从而实现对这个特定索引元素的变化监听。
- 当通过
-
局限性
- 无法自动监听所有元素:这种方式需要为每个要监听的索引单独使用
Object.defineProperty进行定义。如果数组长度是动态变化的,或者要监听整个数组,这种逐个定义的方式就非常繁琐且不实用。例如,对于一个有很多元素的数组或者长度会不断变化的数组,几乎不可能预先为每个可能的索引都定义属性描述符。 - 无法直接监听数组方法:它不能直接监听数组的方法(如
push、pop、shift、unshift、splice等)引起的变化。这些方法会改变数组的状态,但不会触发通过Object.defineProperty为数组元素定义的get和set操作。比如,当使用push方法添加元素到数组时,不会自动触发之前为数组元素定义的set操作来监听这个新元素的添加。
- 无法自动监听所有元素:这种方式需要为每个要监听的索引单独使用
-
解决方案 - 重写数组方法实现全面监听
以下是使用Object.defineProperty来实现监听数组部分常见操作(如修改元素、添加元素、删除元素等)的基本思路和示例代码:
3.1. 整体思路
要使用Object.defineProperty监听数组,主要思路是对数组的原型方法进行重定义,在这些重定义的方法内部,通过Object.defineProperty来设置属性描述符,使得在执行这些操作时能够触发自定义的监听函数,从而实现对数组变化的监听。
3.2. 具体步骤及示例代码
(1)创建一个继承自原生数组的新类
首先,创建一个新的类,让它继承自原生数组,以便后续可以在这个新类上添加自定义的监听逻辑。
function ObservableArray() {
// 调用原生数组构造函数,确保可以像正常数组一样使用
Array.apply(this, arguments);
}
ObservableArray.prototype = Object.create(Array.prototype);
ObservableArray.prototype.constructor = ObservableArray;
(2)重定义数组的部分原型方法
接下来,重定义数组的一些常见操作的原型方法,比如push、pop、shift、unshift、splice等,在这些重定义的方法内部添加监听逻辑。
以push方法为例:
ObservableArray.prototype.push = function () {
// 保存当前数组长度,用于后续判断添加了几个元素
var previousLength = this.length;
// 调用原生数组的push方法,执行实际的添加操作
var result = Array.prototype.push.apply(this, arguments);
// 遍历新添加的元素,为每个元素设置属性描述符以实现监听
for (var i = previousLength; i < this.length; i++) {
(function (index) {
Object.defineProperty(this, index, {
enumerable: true,
configurable: true,
get: function () {
console.log("正在读取索引为" + index + "的元素");
return this[index];
},
set: function (value) {
console.log("正在设置索引为" + index + "的元素为" + value);
this[index] = value;
},
});
}).call(this, i);
}
console.log("执行了push操作,添加了" + (this.length - previousLength) + "个元素");
return result;
};
在上述push方法的重定义中:
- 首先调用原生数组的
push方法来执行实际的添加元素操作,并保存添加前的数组长度。 - 然后,通过循环为新添加的每个元素使用
Object.defineProperty设置属性描述符。在get方法中,当读取该元素时会打印相应信息;在set方法中,当设置该元素时也会打印相应信息。 - 最后,打印出执行
push操作添加的元素个数。
类似地,可以重定义其他如pop、shift、unshift、splice等方法,以下是pop方法的重定义示例:
ObservableArray.prototype.pop = function () {
var result = Array.prototype.pop.apply(this, arguments);
if (this.length >= 0) {
Object.defineProperty(this, this.length, {
enumerable: true,
configurable: true,
get: function () {
console.log("正在读取最后一个元素");
return this[this.length];
},
set: function (value) {
console.log("正在设置最后一个元素为" + value);
this[this.length] = value;
},
});
}
console.log("执行了pop操作");
return result;
};
3.3. 使用示例
创建ObservableArray的实例并进行操作来测试监听效果:
var myArray = new ObservableArray(1, 2, 3);
myArray.push(4, 5);
var poppedElement = myArray.pop();
myArray[0] = 10;
在上述示例中:
-
首先创建了一个
ObservableArray实例myArray并初始化为[1, 2, 3]。 -
然后执行
myArray.push(4, 5),此时会触发重定义的push方法,添加元素的同时会为新添加的元素设置监听逻辑,并且会打印出相关操作信息。 -
接着执行
myArray.pop(),触发重定义的pop方法,执行弹出操作并设置对最后一个元素的监听逻辑,同时打印出相关操作信息。 -
最后执行
myArray[0] = 10,由于之前为数组元素设置了监听逻辑(在push方法中对新添加元素设置了监听),所以会触发相应的set逻辑,打印出相关信息。
1067. 项目部署更新之后,如何提醒用户去刷新更新页面资源【热度: 340】【web应用场景】【出题公司: 百度】
- 使用版本号查询参数
- 原理:在 HTML 文件中,对于引用的静态资源(如 JavaScript 文件、CSS 文件),可以在 URL 后面添加一个版本号查询参数。每次更新项目后,更新这个版本号。这样,浏览器会将带有新的版本号的资源视为一个新的请求,从而强制刷新资源。
- 示例:
- 原始的 JavaScript 文件引用可能是
<script src="main.js"></script>。更新后,可以将其改为<script src="main.js?v=2"></script>,其中v=2是版本号。每次更新项目时,递增这个版本号。 - 在构建或部署工具中,可以通过配置自动更新这个版本号。例如,在使用 Webpack 构建时,可以使用
html - webpack - plugin插件结合版本控制工具来自动更新版本号。假设你有一个简单的 Webpack 配置文件,如下:
const HtmlWebpackPlugin = require("html - webpack - plugin"); const webpack = require("webpack"); const path = require("path"); module.exports = { entry: "./src/index.js", output: { filename: "main.js", path: path.resolve(__dirname, "dist"), }, plugins: [ new HtmlWebpackPlugin({ template: "index.html", // 自定义版本号,这里可以通过读取文件系统中的版本文件或者其他方式来获取真实的版本号 version: "2", }), new webpack.BannerPlugin("版权所有,翻版必究"), ], };- 这个配置文件中的
HtmlWebpackPlugin插件可以将版本号插入到 HTML 文件中引用的静态资源 URL 中。
- 原始的 JavaScript 文件引用可能是
- 利用浏览器缓存清除技术(Service Workers)
- 原理:Service Workers 是一种在浏览器后台运行的脚本,它可以拦截和处理网络请求。通过 Service Workers,可以控制浏览器缓存的清除和更新。当项目更新后,Service Workers 可以检测到新的资源版本,并通知浏览器清除旧的缓存,从而强制刷新页面资源。
- 示例步骤:
- 首先,在网页中注册 Service Workers:
if ("serviceWorker" in navigator) { navigator.serviceWorker .register("service - worker.js") .then(function (registration) { console.log("Service Worker注册成功"); }) .catch(function (error) { console.log("Service Worker注册失败: ", error); }); }- 然后,在
service - worker.js文件中编写缓存管理逻辑。例如,以下是一个简单的缓存更新逻辑:
self.addEventListener("fetch", function (event) { event.respondWith( caches.match(event.request).then(function (response) { if (response) { return response; } return fetch(event.request); }) ); }); self.addEventListener("activate", function (event) { var cacheWhitelist = ["v2 - cache"]; event.waitUntil( caches.keys().then(function (cacheNames) { return Promise.all( cacheNames.map(function (cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); });- 这里的
activate事件处理函数用于在 Service Workers 激活时,清除旧的缓存(不在cacheWhitelist中的缓存)。fetch事件处理函数用于拦截网络请求,首先尝试从缓存中获取资源,如果缓存中没有,则从网络中获取。
- 通过 WebSockets 或长轮询进行实时通知(较复杂)
- 原理:建立一个服务器到客户端的实时通信通道,如 WebSockets 或长轮询。当项目更新时,服务器通过这个通道发送一个通知到客户端,客户端收到通知后,刷新页面或者重新加载相关资源。
- 示例(以 WebSockets 为例):
- 首先,在服务器端(假设使用 Node.js 和
ws库)建立 WebSockets 服务:
const WebSocket = require("ws"); const wss = new WebSocket.Server({ port: 8080 }); wss.on("connection", function connection(ws) { console.log("客户端已连接"); // 当项目更新时,发送更新通知 ws.send("update"); });- 然后,在客户端建立 WebSockets 连接并监听更新通知:
const socket = new WebSocket("ws://localhost:8080"); socket.addEventListener("message", function (event) { if (event.data === "update") { location.reload(); } });-
这种方法比较复杂,需要在服务器端和客户端都进行额外的配置,但可以实现非常及时的更新通知。
- 首先,在服务器端(假设使用 Node.js 和
1070. Webpack 与 Vite的核心差异点在哪儿【热度: 610】【工程化】
关键词:Webpack 与 Vite 核心差异
-
构建原理差异
- Webpack:
- 基于打包(Bundle)的理念。在处理模块时,Webpack 会从一个入口文件开始,递归地构建整个模块依赖图。它会把所有的模块(包括 JavaScript、CSS、图片等各种资源)都打包到一个或多个 bundle 文件中。例如,对于一个简单的 JavaScript 应用,Webpack 会分析
import和require语句,将所有相关的模块代码收集起来,经过一系列的转换(如 babel 编译、代码压缩等)后,生成最终的打包文件。这个过程涉及到大量的模块解析、加载和转换,在处理大型项目时,构建时间可能会较长。 - 采用模块热替换(HMR - Hot Module Replacement)技术来提高开发体验。在开发过程中,当修改了某个模块的代码时,Webpack 可以只更新受影响的模块,而不是重新加载整个页面。不过,Webpack 的 HMR 配置相对复杂,需要针对不同类型的模块进行专门的配置。
- 基于打包(Bundle)的理念。在处理模块时,Webpack 会从一个入口文件开始,递归地构建整个模块依赖图。它会把所有的模块(包括 JavaScript、CSS、图片等各种资源)都打包到一个或多个 bundle 文件中。例如,对于一个简单的 JavaScript 应用,Webpack 会分析
- Vite:
- 基于原生 ES 模块(ESM)的开发服务器。在开发阶段,Vite 利用浏览器对 ES 模块的原生支持,不需要打包操作。当浏览器请求一个模块时,Vite 直接将对应的文件发送给浏览器,浏览器通过
<script type="module">标签来加载和解析这些模块。这使得 Vite 在开发阶段的启动速度非常快,因为它避免了像 Webpack 那样预先打包的过程。 - 对于生产环境,Vite 会进行预构建(Pre - build),主要是为了将一些非 ESM 格式的依赖(如 CommonJS 模块)转换为 ESM 格式,以确保在浏览器环境中能够高效地加载和运行。预构建的过程相对简单快速,并且只会对需要转换的依赖进行处理,不会像 Webpack 那样对整个项目进行全面打包。
- 基于原生 ES 模块(ESM)的开发服务器。在开发阶段,Vite 利用浏览器对 ES 模块的原生支持,不需要打包操作。当浏览器请求一个模块时,Vite 直接将对应的文件发送给浏览器,浏览器通过
- Webpack:
-
开发体验差异
- 启动速度:
- Vite:在开发模式下,由于不需要进行完整的打包过程,Vite 的启动速度极快。它可以在瞬间启动开发服务器,让开发者能够快速地开始开发工作。例如,一个中等规模的项目,Vite 可能在几百毫秒内就可以启动开发服务器,而 Webpack 可能需要几秒甚至更长时间。
- Webpack:启动速度相对较慢,因为它需要在启动时构建整个模块依赖图并进行打包操作。特别是对于大型项目,这个过程可能会花费较多时间,影响开发效率。
- 模块热替换(HMR)体验:
- Vite:Vite 的 HMR 实现相对简单高效。由于它基于原生 ES 模块,在更新模块时,浏览器可以更快地获取和更新新的模块代码。并且 Vite 的 HMR 配置相对简单,对于大多数常见的模块类型,都能很好地支持热替换,减少了开发者在配置上花费的精力。
- Webpack:虽然 Webpack 也支持 HMR,但它的配置较为复杂,需要针对不同类型的模块(如样式模块、JavaScript 模块等)进行专门的配置,才能实现较好的热替换效果。而且在某些复杂的场景下,Webpack 的 HMR 可能会出现更新不及时或者不完全的情况。
- 启动速度:
-
性能差异
- 开发阶段性能:
- Vite:在开发阶段,由于其基于原生 ES 模块的加载方式,浏览器可以并行加载多个模块,提高了开发阶段的页面加载速度。同时,因为不需要打包,减少了构建过程对开发机器资源的占用,使得开发过程更加流畅。
- Webpack:开发阶段的性能主要受限于打包过程。每次修改代码后的重新打包可能会占用较多的 CPU 和内存资源,尤其是在大型项目中,这可能会导致开发机器出现卡顿现象。而且打包后的文件体积可能较大,影响页面的首次加载速度。
- 生产阶段性能:
- Vite:在生产阶段,Vite 会对代码进行优化,如压缩、代码分割等。虽然它的预构建过程相对简单,但通过合理的配置和优化,也可以生成性能良好的生产包。并且由于其在开发阶段已经对模块进行了一定的优化处理(如将非 ESM 模块转换为 ESM),生产阶段的构建过程相对高效。
- Webpack:Webpack 在生产阶段有丰富的优化插件和策略,如 Tree - Shaking(摇树优化)、代码压缩、懒加载等,可以有效地减小打包文件的体积,提高应用在生产环境中的性能。不过,这些优化过程可能会增加构建的复杂性和时间成本。
- 开发阶段性能:
-
生态系统和插件支持差异
- Webpack:
- 拥有庞大且成熟的插件生态系统。经过多年的发展,Webpack 有各种各样的插件可以用于不同的目的,如代码优化、资源管理、国际化等。开发者可以很容易地找到满足自己需求的插件,并且这些插件的文档和社区支持通常比较完善。
- 插件的开发和使用相对复杂。由于 Webpack 的插件机制涉及到复杂的钩子(Hook)系统,开发一个新的插件需要对 Webpack 的内部机制有深入的了解。同时,在使用插件时,也需要注意插件之间的兼容性和配置顺序。
- Vite:
-
插件生态系统相对较新但发展迅速。Vite 的插件主要用于在预构建阶段或者开发服务器运行过程中对模块进行处理,如对特定类型的资源进行转换或者优化。虽然目前插件数量不如 Webpack 多,但对于主流的功能需求,已经有了相应的插件支持。
-
插件开发相对简单。Vite 的插件机制基于 Rollup(Vite 在生产构建阶段使用 Rollup),对于熟悉 Rollup 插件开发的开发者来说,很容易上手 Vite 插件的开发。并且 Vite 插件的配置和使用通常比较直观。
-
- Webpack:
1071. Webpack 与 Vite 在产物结果上有何区别【热度: 420】【工程化】
关键词:Webpack 与 Vite 产物差异
-
模块格式和加载方式
- Webpack:
- 在产物中,Webpack 通常会将多个模块打包成一个或多个 bundle 文件。这些 bundle 文件的格式可以是多种形式,如 CommonJS(在 Node.js 环境下常用)或者 IIFE(立即调用函数表达式,用于浏览器环境)。对于浏览器环境,Webpack 会将所有的 JavaScript 模块打包成一个或几个大的文件,并且通过自定义的加载逻辑来加载模块。例如,在一个简单的 Webpack 打包后的 JavaScript 文件中,可能会看到类似
__webpack_require__这样的函数来实现模块的加载和解析。 - 这种打包方式会把所有的模块依赖关系都内建在 bundle 文件中,使得浏览器在加载时只需要加载一个或几个文件即可。但如果应用规模较大,bundle 文件可能会变得非常大,导致首次加载时间过长。为了解决这个问题,Webpack 也支持代码分割(Code Splitting),通过动态
import等方式将代码分割成多个较小的块,在需要的时候再加载。
- 在产物中,Webpack 通常会将多个模块打包成一个或多个 bundle 文件。这些 bundle 文件的格式可以是多种形式,如 CommonJS(在 Node.js 环境下常用)或者 IIFE(立即调用函数表达式,用于浏览器环境)。对于浏览器环境,Webpack 会将所有的 JavaScript 模块打包成一个或几个大的文件,并且通过自定义的加载逻辑来加载模块。例如,在一个简单的 Webpack 打包后的 JavaScript 文件中,可能会看到类似
- Vite:
- Vite 在产物中更倾向于保留原生 ES 模块(ESM)的格式。在开发阶段,Vite 利用浏览器对 ES 模块的原生支持来加载模块,而在生产阶段,它也会尽量保持这种格式。这意味着浏览器可以利用原生的
<script type="module">标签来加载模块,并且可以根据模块的依赖关系自动地加载相关的模块。 - 不过,Vite 也会对一些非 ESM 格式的依赖进行预构建,将它们转换为 ESM 格式。在最终的产物中,这些经过转换的依赖也会以符合 ES 模块规范的形式出现,使得整个应用在浏览器中的加载和运行更加高效。
- Vite 在产物中更倾向于保留原生 ES 模块(ESM)的格式。在开发阶段,Vite 利用浏览器对 ES 模块的原生支持来加载模块,而在生产阶段,它也会尽量保持这种格式。这意味着浏览器可以利用原生的
- Webpack:
-
代码优化程度
- Webpack:
- 有一套成熟的代码优化机制。在生产构建过程中,Webpack 可以通过 Tree - Shaking(摇树优化)技术去除没有被使用的代码。例如,如果一个 JavaScript 库中有很多模块,但在应用中只使用了其中一部分,Webpack 可以在打包时将未使用的模块代码移除,从而减小打包文件的体积。
- 还可以进行代码压缩和混淆。通过插件(如 TerserPlugin)可以对 JavaScript 代码进行压缩,去除空格、缩短变量名等操作,进一步减小文件大小。同时,Webpack 还支持对 CSS 等其他资源进行优化,如压缩 CSS 文件、提取 CSS 到单独的文件等操作。
- Vite:
- 同样也会进行代码优化。Vite 在生产构建时也会进行代码压缩和 Tree - Shaking。它利用 Esbuild 等工具来快速地进行这些优化操作。Esbuild 是一个高性能的 JavaScript 打包工具,能够在短时间内完成代码的压缩和优化。
- 由于 Vite 在开发阶段已经对模块进行了一定的处理(如将非 ESM 模块转换为 ESM),在生产阶段的优化过程相对更加高效。而且 Vite 的优化策略也在不断发展和完善,以提供与 Webpack 相当甚至更好的优化效果。
- Webpack:
-
文件结构和大小分布
- Webpack:
- 生成的文件结构可能相对复杂。尤其是在进行了代码分割之后,会产生多个 bundle 文件,这些文件可能包括主入口文件、异步加载的模块文件、CSS 文件(如果通过插件提取到单独的文件)等。文件大小分布可能不均匀,主入口文件可能会比较大,包含了大部分的核心模块和公共模块,而异步加载的模块文件大小则根据具体的功能模块而不同。
- 对于大型项目,Webpack 可能会生成较大的 bundle 文件,即使经过优化,由于其打包的特性,文件大小仍然可能是一个需要关注的问题。需要通过合理的代码分割和懒加载策略来优化文件大小和加载顺序,以提高应用的性能。
- Vite:
-
文件结构相对简单。因为 Vite 更倾向于以 ES 模块的方式组织代码,在产物中,每个模块基本上都以独立的文件形式存在(或者是经过预构建后的模块组),并且可以根据需要自动加载。文件大小分布相对均匀,每个模块文件大小根据其自身的功能和代码量而定。
-
这种文件结构使得浏览器可以根据实际需要加载模块,避免了一次性加载大量不必要的代码。不过,在一些情况下,如果没有合理地管理模块,可能会导致过多的小文件,增加了浏览器的请求次数,需要在模块组织和文件合并等方面进行平衡。
-
- Webpack:
1072. [Webpack] 我如何实现在高版本浏览器上使用 es6 产物, 在低版本上使用 es5【热度: 122】【工程化】
关键词:Webpack es6 产物
-
分别打包 ES5 和 ES6 产物(从一份 ES6 源码)
- 配置 Webpack 和 Babel 基础环境
- 首先,安装必要的依赖。除了 Webpack 本身相关的依赖外,还需要
babel - loader、@babel/core和@babel/preset - env。
npm install webpack webpack - cli babel - loader @babel/core @babel/preset - env --save - dev- 创建 Webpack 配置文件(例如
webpack.config.js),并在其中配置基本的模块规则,用于处理 JavaScript 文件。先配置一个简单的 ES6 打包规则:
const path = require("path"); module.exports = { entry: "./src/index.js", // 假设ES6源码入口文件是index.js output: { filename: "es6 - bundle.js", path: path.resolve(__dirname, "dist/es6"), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel - loader", options: { presets: [ [ "@babel/preset - env", { targets: { // 设置一个较高的浏览器版本支持ES6原生,这里假设现代浏览器都支持 chrome: "80", firefox: "70", }, }, ], ], }, }, }, ], }, }; - 首先,安装必要的依赖。除了 Webpack 本身相关的依赖外,还需要
- 添加 ES5 打包配置
- 为了打包 ES5 产物,在 Webpack 配置文件中添加另一个配置对象。可以通过
webpack - merge工具(需要安装webpack - merge)来合并基础配置和 ES5 特定配置,避免重复配置。
const path = require("path"); const webpackMerge = require("webpack - merge"); const baseConfig = { entry: "./src/index.js", output: { filename: "es6 - bundle.js", path: path.resolve(__dirname, "dist/es6"), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel - loader", options: { presets: [ [ "@babel/preset - env", { targets: { chrome: "80", firefox: "70", }, }, ], ], }, }, }, ], }, }; const es5Config = { output: { filename: "es5 - bundle.js", path: path.resolve(__dirname, "dist/es5"), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel - loader", options: { presets: [ [ "@babel/preset - env", { targets: { // 设置较低的浏览器版本,确保转换为ES5语法 chrome: "50", firefox: "40", }, useBuiltIns: "usage", corejs: 3, }, ], ], }, }, }, ], }, }; module.exports = [baseConfig, webpackMerge(baseConfig, es5Config)];- 上述配置中,
es5Config部分重新定义了输出文件名和路径,并且在babel - loader的选项中,通过@babel/preset - env的targets选项设置了较低的浏览器支持范围,这样就可以将源码转换为 ES5 语法并打包到dist/es5目录下的es5 - bundle.js文件。
- 为了打包 ES5 产物,在 Webpack 配置文件中添加另一个配置对象。可以通过
- 配置 Webpack 和 Babel 基础环境
-
区分加载 ES5 还是 ES6 产物
-
使用浏览器特性检测脚本(HTML + JavaScript)
- 在 HTML 文件(假设是
index.html)中,可以添加一段 JavaScript 代码来检测浏览器是否支持 ES6 特性。例如,检测Promise是否存在(Promise是 ES6 的一个典型特性)。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> </head> <body> <script> if (typeof Promise === "undefined") { document.write('<script src="dist/es5/es5 - bundle.js"><\/script>'); } else { document.write('<script src="dist/es6/es6 - bundle.js"><\/script>'); } </script> </body> </html>- 这种方法简单直接,但有一定的局限性。如果浏览器对 ES6 的支持不完全(例如,支持部分 ES6 语法但不支持检测的特性),可能会导致加载错误的产物。
- 在 HTML 文件(假设是
-
使用服务端渲染(SSR)或构建时检测(更高级)
- 如果是在服务端渲染的应用中,可以在服务端根据用户代理(User - Agent)来判断浏览器版本,进而选择加载 ES5 还是 ES6 产物。这需要在服务端代码(例如 Node.js 中的 Express 应用)中进行逻辑处理。
- 另一种构建时检测的方法是,在构建过程中生成一个包含浏览器支持信息的配置文件。可以使用
browserslist工具(@babel/preset - env内部也使用了这个工具来确定目标浏览器)结合一些自定义脚本,在构建时确定目标浏览器范围,然后生成一个配置文件(例如browser - config.json),内容可能是{"supportsES6": true}或者{"supportsES6": false}。在 HTML 文件中,通过读取这个配置文件来加载相应的产物。
// 假设这是一个自定义脚本,用于生成browser - config.json const browserslist = require("browserslist"); const fs = require("fs"); const targets = { chrome: "50", firefox: "40", }; const browsers = browserslist(Object.keys(targets).map((browser) => `${browser} >= ${targets[browser]}`)); const supportsES6 = browsers.some( (browser) => browser.includes("Chrome >= 80") || browser.includes("Firefox >= 70") ); fs.writeFileSync("browser - config.json", JSON.stringify({ supportsES6 }));- 然后在 HTML 文件中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <script> const browserConfig = JSON.parse(document.currentScript.getAttribute("data - config")); if (browserConfig.supportsES6) { document.write('<script src="dist/es6/es6 - bundle.js"><\/script>'); } else { document.write('<script src="dist/es5/es5 - bundle.js"><\/script>'); } </script> <script src="browser - config.json" data - config></script> </body> </html>
-
1074. [webpack] 在编译产物的时候, 要区分 source 代码和外部依赖代码, 该如何处理【热度: 125】【工程化】
关键词:webpack 产物
-
使用 Webpack 的
optimize-module-ids插件(用于区分模块来源)-
原理:Webpack 在打包过程中会为每个模块分配一个唯一的
module.id。optimize-module-ids插件可以帮助控制模块标识符的生成方式,使得能够根据模块是源文件还是外部依赖来区分它们。 -
配置步骤:
- 首先,安装
optimize-module-ids插件(可能需要自行开发类似功能插件或寻找已有合适插件)。 - 然后,在 Webpack 配置文件中添加插件配置。例如:
const CustomModuleIdsPlugin = require("optimize-module-ids"); module.exports = { //...其他配置 plugins: [ new CustomModuleIdsPlugin((module) => { if (module.resource && module.resource.includes("node_modules")) { return "external"; } else { return "source"; } }), ], };这个插件会依据模块的资源路径(
module.resource)来判别模块是源自node_modules(外部依赖)还是其他源文件路径。若为外部依赖,模块的id会被标记为external,否则标记为source。如此一来,在最终的打包产物或构建信息里,就能通过这个id区分不同来源的模块。 - 首先,安装
-
-
通过构建工具的输出信息区分(适用于简单区分)
- 查看构建日志:Webpack 在构建过程中会输出大量的日志信息。可在构建日志里查找模块的路径信息以区分源文件和外部依赖。比如,日志中来自
src目录的模块通常是源文件,而来自node_modules目录的模块则是外部依赖。 - 分析统计信息(
stats):Webpack 提供了stats选项,可生成详细的构建统计信息。通过将stats配置为'verbose'或其他详细级别,能获取每个模块的路径、大小、依赖关系等信息。在这些信息中,可轻易识别出源文件和外部依赖。例如,配置stats如下:
module.exports = { //...其他配置 stats: "verbose", };之后便可通过分析生成的统计文件或在终端输出的详细统计信息来区分不同来源的模块。
- 查看构建日志:Webpack 在构建过程中会输出大量的日志信息。可在构建日志里查找模块的路径信息以区分源文件和外部依赖。比如,日志中来自
-
自定义打包结构或命名规则(在输出阶段区分)
- 分离输出目录:在 Webpack 的输出配置(
output)中,可以设置不同的输出路径来分离源文件和外部依赖的打包产物。例如:
module.exports = { //...其他配置 output: { path: path.resolve(__dirname, "dist"), filename: (chunkData) => { if (chunkData.chunk.name.includes("external")) { return "external-bundles/[name].js"; } else { return "source-bundles/[name].js"; } }, }, };这里依据模块所属的
chunk名称(可在构建过程中通过某些方式将模块所属的chunk标记为external或source),把外部依赖和源文件分别打包到不同的目录(external-bundles和source-bundles)下,这样在最终的打包产物中就能很直观地进行区分。-
命名规则:除了分离输出目录,还可通过命名规则来区分。例如,在输出文件名中添加前缀以表示模块来源,如
source-[name].js和external-[name].js,如此在查看打包文件时就能快速识别模块来源。
- 分离输出目录:在 Webpack 的输出配置(
1075. [webpack] externals 是如何加载外部依赖的【热度: 330】【工程化】
关键词:webpack externals
-
externals基础原理- 当在 Webpack 配置文件中使用
externals选项时,实际上是在告诉 Webpack 某些模块应该被视为外部依赖,而不是被打包进最终的输出文件。这意味着这些模块将在运行时由浏览器或其他运行环境提供,而不是由 Webpack 处理。
- 当在 Webpack 配置文件中使用
-
在浏览器环境中的加载方式(以全局变量为例)
- 配置
externals:- 假设项目依赖于
lodash库,并且希望在浏览器环境中通过<script>标签加载lodash的全局变量。首先在 Webpack 配置文件中这样配置externals:
module.exports = { //...其他配置 externals: { lodash: "lodash", }, }; - 假设项目依赖于
- HTML 文件中的脚本引入:
- 然后在 HTML 文件中,需要手动添加
<script>标签来引入lodash。例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script> <script src="your - main - app - bundle.js"></script> </body> </html>- 在这里,
your - main - app - bundle.js是 Webpack 打包后的应用程序主文件。当 Webpack 在代码中遇到import _ from 'lodash';语句时,它不会将lodash的代码打包进输出文件,而是假设lodash已经在运行时环境中存在,并且可以通过全局变量lodash访问。所以在打包后的 JavaScript 代码中,实际上是通过全局变量来引用外部依赖的。
- 然后在 HTML 文件中,需要手动添加
- 配置
-
在其他运行环境(如 Node.js)中的加载方式(以 CommonJS 模块为例)
- 配置
externals类似操作:- 在 Webpack 配置文件中,对于要作为外部依赖处理的模块(假设是
axios,用于在 Node.js 环境中进行 HTTP 请求),配置externals如下:
module.exports = { //...其他配置 externals: { axios: "axios", }, }; - 在 Webpack 配置文件中,对于要作为外部依赖处理的模块(假设是
- 运行时环境中的模块引用:
- 在 Node.js 应用程序代码中,需要确保
axios已经作为一个 CommonJS 模块安装在项目的node_modules目录下或者在全局环境中有相应的模块路径。当运行打包后的代码时,Webpack 不会打包axios,而是期望在 Node.js 的模块加载机制中找到它。例如,在打包后的 Node.js 代码中可能会有类似这样的引用:
const axios = require("axios");- 此时,Node.js 会按照自己的模块加载规则去查找
axios模块,就像没有经过 Webpack 打包一样。这是因为 Webpack 通过externals配置将axios模块的加载责任交给了运行时环境的模块加载系统。
- 在 Node.js 应用程序代码中,需要确保
- 配置
-
AMD(Asynchronous Module Definition)模块加载方式(适用于支持 AMD 的环境)
- 配置
externals和模块定义:- 假设在一个支持 AMD 的浏览器环境或者 JavaScript 运行环境中,有一个名为
backbone的外部依赖,在 Webpack 配置文件中设置externals:
module.exports = { //...其他配置 externals: { backbone: "backbone", }, }; - 假设在一个支持 AMD 的浏览器环境或者 JavaScript 运行环境中,有一个名为
- AMD 模块加载脚本:
- 在 HTML 文件或者 AMD 模块加载器的配置文件中,需要使用 AMD 的方式来加载
backbone模块。例如,在一个简单的 AMD 配置中,可能会有如下代码来加载backbone和应用程序主模块:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <script src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script> <script> require.config({ paths: { backbone: "https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min", }, }); require(["your - main - app - module", "backbone"], function (app, backbone) { // 应用程序主模块和backbone模块都加载完成后执行的代码 app.init(); }); </script> </body> </html>-
这里使用
require.js作为 AMD 模块加载器,通过require.config配置backbone模块的路径,然后在require函数中加载应用程序主模块和backbone模块。Webpack 在打包过程中,由于externals配置,知道backbone是外部依赖,会正确地处理代码中的引用,使得在运行时可以通过 AMD 模块加载器来加载backbone模块并正确地执行应用程序。
- 在 HTML 文件或者 AMD 模块加载器的配置文件中,需要使用 AMD 的方式来加载
- 配置
1076. vite 和 rollup 是什么样的依赖关系【热度: 153】【工程化】
关键词:vite 和 rollup
-
Vite 对 Rollup 的依赖关系(主要在生产构建阶段)
- 构建过程的底层调用:Vite 在生产构建过程中依赖 Rollup 来打包代码。当执行
vite build命令时,Vite 会在内部调用 Rollup 来处理模块打包任务。Rollup 会按照 Vite 提供的配置信息(如入口文件、输出格式、插件等)对项目中的 ES 模块(ESM)进行处理。 - 配置共享与交互:Vite 的配置文件(
vite.config.js)中的build.rollupOptions部分用于向 Rollup 传递配置。这意味着 Vite 的构建配置可以直接影响 Rollup 的打包行为。例如,通过配置rollupOptions中的input指定入口文件,output指定输出格式(如umd、es、csm等)和文件路径,以及添加 Rollup 插件来扩展功能。这种配置共享机制表明 Vite 在构建过程中深度依赖 Rollup 的打包功能,并通过配置来定制打包流程。 - 功能利用与优化协作:Vite 利用 Rollup 的诸多优化功能来生成高质量的生产代码。其中,Tree - Shaking(摇树优化)是 Rollup 的一个关键特性,Vite 借助这一功能去除未使用的代码,减小打包文件的大小。另外,Rollup 的作用域提升(Scope Hoisting)功能也有助于提高代码性能,Vite 在构建时通过 Rollup 实现这一优化,使得代码在浏览器中的执行更加高效。
- 构建过程的底层调用:Vite 在生产构建过程中依赖 Rollup 来打包代码。当执行
-
Rollup 相对独立于 Vite 的存在
- Rollup 自身的功能完整性:Rollup 本身是一个独立的 JavaScript 模块打包工具,它具有自己的一套完整功能体系。它可以独立于 Vite 使用,开发者可以直接使用 Rollup 来打包 JavaScript 库或应用,通过编写 Rollup 配置文件(
rollup.config.js)来指定打包规则,包括入口文件、输出格式、插件使用等。例如,许多 JavaScript 库的作者使用 Rollup 来打包他们的代码,以生成高效、简洁的库文件供其他开发者使用。 - 应用场景的差异:Rollup 主要聚焦于模块打包和代码优化,适用于各种需要将 ES 模块转换为可部署格式的场景。而 Vite 除了在生产构建时使用 Rollup 进行打包外,更侧重于开发过程中的快速开发体验,如利用浏览器原生 ES 模块支持实现快速的模块加载和热替换(HMR)。这意味着在开发模式下,Vite 的功能与 Rollup 有明显的区别,Rollup 在这个阶段通常不参与 Vite 的主要功能实现。
- Rollup 自身的功能完整性:Rollup 本身是一个独立的 JavaScript 模块打包工具,它具有自己的一套完整功能体系。它可以独立于 Vite 使用,开发者可以直接使用 Rollup 来打包 JavaScript 库或应用,通过编写 Rollup 配置文件(
-
插件生态中的关联与差异
-
插件共享基础:Vite 在生产构建阶段与 Rollup 的插件生态有一定的关联。部分 Rollup 插件可以在 Vite 的生产构建过程中直接使用或者经过适当修改后使用。这是因为 Vite 在生产构建底层基于 Rollup,它们在插件机制上有一些相似之处,例如在代码压缩、模块转换等方面的插件功能可以共享。
-
插件开发的侧重点差异:尽管有共享插件的可能性,但 Vite 和 Rollup 的插件开发也有不同的侧重点。Vite 插件可能需要考虑更多与开发服务器、即时编译、样式处理等开发过程相关的功能,而 Rollup 插件更侧重于模块打包和优化环节,如自定义模块解析、高级的 Tree - Shaking 策略等。
-
1077. Rollup 为何高效【热度: 200】【工程化】
关键词:Rollup 构建效率
关键词:vite 和 rollup
-
Vite 对 Rollup 的依赖关系(主要在生产构建阶段)
- 构建过程的底层调用:Vite 在生产构建过程中依赖 Rollup 来打包代码。当执行
vite build命令时,Vite 会在内部调用 Rollup 来处理模块打包任务。Rollup 会按照 Vite 提供的配置信息(如入口文件、输出格式、插件等)对项目中的 ES 模块(ESM)进行处理。 - 配置共享与交互:Vite 的配置文件(
vite.config.js)中的build.rollupOptions部分用于向 Rollup 传递配置。这意味着 Vite 的构建配置可以直接影响 Rollup 的打包行为。例如,通过配置rollupOptions中的input指定入口文件,output指定输出格式(如umd、es、csm等)和文件路径,以及添加 Rollup 插件来扩展功能。这种配置共享机制表明 Vite 在构建过程中深度依赖 Rollup 的打包功能,并通过配置来定制打包流程。 - 功能利用与优化协作:Vite 利用 Rollup 的诸多优化功能来生成高质量的生产代码。其中,Tree - Shaking(摇树优化)是 Rollup 的一个关键特性,Vite 借助这一功能去除未使用的代码,减小打包文件的大小。另外,Rollup 的作用域提升(Scope Hoisting)功能也有助于提高代码性能,Vite 在构建时通过 Rollup 实现这一优化,使得代码在浏览器中的执行更加高效。
- 构建过程的底层调用:Vite 在生产构建过程中依赖 Rollup 来打包代码。当执行
-
Rollup 相对独立于 Vite 的存在
- Rollup 自身的功能完整性:Rollup 本身是一个独立的 JavaScript 模块打包工具,它具有自己的一套完整功能体系。它可以独立于 Vite 使用,开发者可以直接使用 Rollup 来打包 JavaScript 库或应用,通过编写 Rollup 配置文件(
rollup.config.js)来指定打包规则,包括入口文件、输出格式、插件使用等。例如,许多 JavaScript 库的作者使用 Rollup 来打包他们的代码,以生成高效、简洁的库文件供其他开发者使用。 - 应用场景的差异:Rollup 主要聚焦于模块打包和代码优化,适用于各种需要将 ES 模块转换为可部署格式的场景。而 Vite 除了在生产构建时使用 Rollup 进行打包外,更侧重于开发过程中的快速开发体验,如利用浏览器原生 ES 模块支持实现快速的模块加载和热替换(HMR)。这意味着在开发模式下,Vite 的功能与 Rollup 有明显的区别,Rollup 在这个阶段通常不参与 Vite 的主要功能实现。
- Rollup 自身的功能完整性:Rollup 本身是一个独立的 JavaScript 模块打包工具,它具有自己的一套完整功能体系。它可以独立于 Vite 使用,开发者可以直接使用 Rollup 来打包 JavaScript 库或应用,通过编写 Rollup 配置文件(
-
插件生态中的关联与差异
-
插件共享基础:Vite 在生产构建阶段与 Rollup 的插件生态有一定的关联。部分 Rollup 插件可以在 Vite 的生产构建过程中直接使用或者经过适当修改后使用。这是因为 Vite 在生产构建底层基于 Rollup,它们在插件机制上有一些相似之处,例如在代码压缩、模块转换等方面的插件功能可以共享。
-
插件开发的侧重点差异:尽管有共享插件的可能性,但 Vite 和 Rollup 的插件开发也有不同的侧重点。Vite 插件可能需要考虑更多与开发服务器、即时编译、样式处理等开发过程相关的功能,而 Rollup 插件更侧重于模块打包和优化环节,如自定义模块解析、高级的 Tree - Shaking 策略等。
-