普通视图
Vue路由传参避坑指南:params和query的那些猫腻
Vue路由跳转的N种姿势,总有一种适合你!
Vue路由玩法大揭秘:三种路由模式你Pick谁?
超好用的vue图片预览插件更新啦,hevue-img-preview 7.0.0版本正式发布,支持vue2/vue3/移动/pc,增加缩略图、下载、自定义样式等
虚拟列表兼容老DOM操作
在前端开发中,为不定高度的虚拟列表处理原有的业务逻辑中涉及的 DOM 操作是一个常见的挑战。虚拟列表的核心思想是只渲染视口内及其附近的一小部分 DOM 元素,从而提高长列表的性能。然而,这与许多传统业务逻辑中直接操作 DOM 的方式产生了冲突,因为那些 DOM 元素可能根本不存在于当前渲染的列表中,或者它们是虚拟列表复用的元素,其内容和状态会随着滚动而变化。
核心问题
当使用虚拟列表时,以下类型的 DOM 操作会变得有问题:
-
直接通过 ID 或选择器查询元素:
document.getElementById('item-id-123')
document.querySelector('.specific-class-on-item')
- 如果
item-id-123
不在当前渲染的视口内,上述查询将返回null
。即使返回了元素,如果虚拟列表复用了 DOM 节点,这个元素可能不再代表你期望的那个逻辑项。
-
直接添加/移除事件监听器:
itemElement.addEventListener('click', handler)
- 当列表项被滚动出视口时,其 DOM 元素可能被销毁或复用。如果事件监听器没有被正确移除,可能导致内存泄漏或事件触发在错误的元素上。
-
直接修改元素的样式或属性:
itemElement.style.backgroundColor = 'red'
itemElement.classList.add('active')
itemElement.setAttribute('data-state', 'expanded')
- 这些修改只作用于当前 DOM 元素。当相同的 DOM 元素被复用以渲染另一个逻辑项时,这些修改会残留,导致显示错误。
-
直接添加/移除子元素:
itemElement.appendChild(newElement)
itemElement.removeChild(existingChild)
- 这会破坏虚拟列表对 DOM 结构的控制,并且当元素被复用时,这些子元素可能不属于新的逻辑项。
解决方案与核心思想
解决这些问题的核心思想是:将 DOM 操作转化为数据操作,并利用事件委托。 虚拟列表应该完全由数据驱动,任何对列表项状态的改变都应该首先反映在数据模型上,然后由虚拟列表组件根据最新的数据重新渲染。
1. 数据驱动 (Data-Driven UI)
- 状态管理: 任何与列表项相关的状态(例如,是否选中、是否展开、是否禁用等)都应该存储在列表的数据源中,而不是直接存储在 DOM 元素的属性或类中。
-
唯一标识: 每个列表项都必须有一个唯一的
id
。这是在数据和 DOM 之间建立映射的关键。 - 渲染函数: 虚拟列表的渲染函数会根据数据源来生成对应的 DOM 结构和样式。当数据更新时,虚拟列表会重新计算可见区域,并重新渲染。
2. 事件委托 (Event Delegation)
- 单一监听器: 不要在每个列表项上单独添加事件监听器。相反,在虚拟列表的父容器上只添加一个事件监听器。
- 事件冒泡: 当子元素上的事件触发时,它会冒泡到父容器。
-
识别目标: 在父容器的事件处理函数中,通过
event.target
或event.target.closest()
来判断是哪个具体的列表项触发了事件,并通过该列表项的dataset
(例如data-id
)获取其唯一的 ID。 - 更新数据: 根据获取到的 ID,更新数据源中对应列表项的状态。
3. 组件化封装 (Component Encapsulation)
-
列表项组件: 将每个列表项的渲染逻辑和内部状态(如果需要)封装成一个独立的组件。这个组件接收数据作为
props
,并负责渲染自己。 - 生命周期: 在组件的生命周期中处理一些特殊的 DOM 交互(例如,如果某个动画或第三方库需要直接操作 DOM,可以在组件挂载时初始化,在组件卸载时清理)。但尽量避免直接操作父组件或兄弟组件的 DOM。
详细代码讲解
我们以一个简单的“不定高度虚拟列表”为例,其中包含“点击切换活跃状态”的业务逻辑。
假设的旧业务逻辑:
// 假设这是旧代码,直接操作 DOM 元素
function oldToggleActiveState(itemId) {
const itemElement = document.getElementById(`item-${itemId}`);
if (itemElement) {
itemElement.classList.toggle('active');
console.log(`Item ${itemId} active state toggled via direct DOM.`);
} else {
console.warn(`Item ${itemId} not found in DOM.`);
}
// 假设还有其他直接操作,比如修改子元素的文本
const titleElement = itemElement.querySelector('.item-title');
if (titleElement) {
titleElement.textContent = `Updated: ${titleElement.textContent}`;
}
}
// 假设每个 item 都有一个点击事件
// document.querySelectorAll('.list-item').forEach(item => {
// item.addEventListener('click', (e) => {
// const itemId = e.currentTarget.dataset.id;
// oldToggleActiveState(itemId);
// });
// });
这种旧逻辑在虚拟列表中会失效,因为 item-${itemId}
可能不存在,或者 document.getElementById
返回的元素是复用的,不代表你期望的那个 itemId
。
虚拟列表的基础结构 (简化版):
我们将构建一个简化的虚拟列表,并展示如何将旧逻辑转化为数据驱动。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Variable Height Virtual List</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
.virtual-list-container {
width: 80%;
height: 600px; /* 固定高度的视口 */
overflow-y: scroll;
border: 1px solid #ccc;
margin: 20px auto;
position: relative; /* 用于定位内部元素 */
background-color: #f9f9f9;
}
.virtual-list-phantom {
/* 占位元素,撑开滚动条高度 */
width: 100%;
position: absolute;
top: 0;
left: 0;
}
.virtual-list-content {
/* 实际渲染内容的容器 */
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.list-item {
padding: 15px;
border-bottom: 1px solid #eee;
background-color: #fff;
box-sizing: border-box;
cursor: pointer;
transition: background-color 0.2s ease;
}
.list-item:hover {
background-color: #f0f0f0;
}
.list-item.active {
background-color: #e6f7ff;
border-left: 5px solid #1890ff;
}
.item-title {
font-weight: bold;
margin-bottom: 5px;
}
.item-content {
font-size: 0.9em;
color: #666;
}
.item-id {
font-size: 0.8em;
color: #999;
margin-top: 5px;
}
</style>
</head>
<body>
<div id="virtual-list" class="virtual-list-container">
<div class="virtual-list-phantom"></div>
<div class="virtual-list-content"></div>
</div>
<script>
// 1. 模拟数据
const initialData = Array.from({ length: 10000 }).map((_, i) => ({
id: i,
title: `Item ${i + 1}`,
content: `This is the content for item ${i + 1}. It can have variable height. ` +
(i % 5 === 0 ? 'This item has a longer description to demonstrate variable height. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' : ''),
isActive: false // 新增状态:是否活跃
}));
// 虚拟列表配置
const container = document.getElementById('virtual-list');
const phantom = container.querySelector('.virtual-list-phantom');
const content = container.querySelector('.virtual-list-content');
const itemBuffer = 5; // 上下缓冲区的项目数量
let itemHeights = new Map(); // 存储每个 item 的高度 { id: height }
let positions = []; // 存储每个 item 的 top 和 bottom 位置 [{ top, bottom, height }]
let data = [...initialData]; // 列表的实际数据,可变
// 2. 计算所有项的初始高度和位置 (离线计算)
// 这是一个关键步骤,因为不定高虚拟列表需要知道每个元素的高度来计算滚动位置。
// 实际应用中,这可能需要一个临时的离屏渲染来测量,或者通过预估 + 动态测量修正。
// 这里为了简化,我们假设可以通过某种方式计算出或预估出高度。
// 在真实场景中,你可能需要一个临时的 DOM 元素来渲染所有项并测量它们的高度。
function calculateAllItemsHeightAndPositions(items) {
positions = [];
let currentOffset = 0;
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.visibility = 'hidden';
tempDiv.style.width = container.clientWidth + 'px'; // 确保宽度一致
document.body.appendChild(tempDiv);
items.forEach(item => {
// 渲染一个临时的 item 来测量高度
tempDiv.innerHTML = `
<div class="list-item ${item.isActive ? 'active' : ''}" data-id="${item.id}">
<div class="item-title">${item.title}</div>
<div class="item-content">${item.content}</div>
<div class="item-id">ID: ${item.id}</div>
</div>
`;
const itemElement = tempDiv.firstChild;
// 确保样式被应用,否则测量不准确
// 强制回流,确保样式计算完成
itemElement.offsetHeight;
const height = itemElement.offsetHeight;
itemHeights.set(item.id, height);
positions.push({
id: item.id,
top: currentOffset,
bottom: currentOffset + height,
height: height
});
currentOffset += height;
});
document.body.removeChild(tempDiv);
phantom.style.height = currentOffset + 'px'; // 设置滚动区域的总高度
}
// 3. 渲染可见区域的列表项
let startIndex = 0;
let endIndex = 0;
let offsetY = 0;
function renderVisibleItems() {
const scrollTop = container.scrollTop;
const viewportHeight = container.clientHeight;
// 找到当前滚动位置对应的起始索引
let startNode = positions.find(pos => pos.bottom > scrollTop);
startIndex = startNode ? positions.indexOf(startNode) : 0;
// 加上缓冲区
startIndex = Math.max(0, startIndex - itemBuffer);
// 计算结束索引
let currentBottom = positions[startIndex].top;
endIndex = startIndex;
while (endIndex < positions.length && (currentBottom - scrollTop) < (viewportHeight + itemBuffer * itemHeights.get(data[0].id || 0))) { // 这里的itemHeights.get(data[0].id || 0) 是一个粗略的平均高度或第一个高度,用于预估
currentBottom += positions[endIndex].height;
endIndex++;
}
endIndex = Math.min(positions.length, endIndex + itemBuffer);
// 计算偏移量
offsetY = positions[startIndex] ? positions[startIndex].top : 0;
// 渲染 DOM
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const itemData = data[i];
if (!itemData) continue; // 数据可能不足,防止越界
const itemDiv = document.createElement('div');
itemDiv.className = `list-item ${itemData.isActive ? 'active' : ''}`; // 根据数据设置 class
itemDiv.dataset.id = itemData.id; // 将 ID 存储在 data 属性中
itemDiv.style.height = itemHeights.get(itemData.id) + 'px'; // 设定高度,避免回流
itemDiv.innerHTML = `
<div class="item-title">${itemData.title}</div>
<div class="item-content">${itemData.content}</div>
<div class="item-id">ID: ${itemData.id}</div>
`;
fragment.appendChild(itemDiv);
}
// 清空旧内容并插入新内容
content.innerHTML = '';
content.appendChild(fragment);
content.style.transform = `translateY(${offsetY}px)`; // 使用 transform 提升性能
}
// 4. 处理滚动事件
let rafId;
function handleScroll() {
if (rafId) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(renderVisibleItems);
}
container.addEventListener('scroll', handleScroll);
// 5. 改造旧业务逻辑:点击切换活跃状态
// 使用事件委托,将监听器添加到父容器
container.addEventListener('click', (event) => {
const clickedItem = event.target.closest('.list-item');
if (clickedItem) {
const itemId = parseInt(clickedItem.dataset.id); // 获取点击项的 ID
console.log(`Clicked item with ID: ${itemId}`);
// 查找并更新数据源中的对应项
const itemIndex = data.findIndex(item => item.id === itemId);
if (itemIndex !== -1) {
// 创建新数组或新对象,避免直接修改原始数据(有利于性能优化和状态管理)
const newData = [...data];
newData[itemIndex] = { ...newData[itemIndex], isActive: !newData[itemIndex].isActive };
data = newData; // 更新数据源
// 重新渲染可见区域
// 注意:如果 isActive 状态会影响高度,则需要重新计算高度和位置
// 在本例中,active 状态只影响颜色和边框,不影响高度,所以不需要重新计算所有高度
// 如果会影响高度,你可能需要:
// 1. 重新测量该项的高度
// 2. 更新 positions 数组中该项及后续所有项的 top/bottom
// 3. 更新 phantom 的高度
// 4. 然后调用 renderVisibleItems()
renderVisibleItems();
}
}
});
// 初始渲染
calculateAllItemsHeightAndPositions(data); // 首次计算所有项的高度和位置
renderVisibleItems();
</script>
</body>
</html>
代码讲解:
-
数据模型 (
initialData
,data
) :- 每个列表项现在是一个包含
id
,title
,content
和isActive
状态的对象。isActive
状态是业务逻辑的核心,它现在存储在数据中。 -
data
数组是虚拟列表渲染的唯一数据源。
- 每个列表项现在是一个包含
-
高度和位置计算 (
calculateAllItemsHeightAndPositions
) :- 这是不定高虚拟列表的关键。它通过创建一个临时的、不可见的 DOM 元素来渲染每个列表项,并测量其真实高度。
-
itemHeights
Map 存储了每个id
对应的高度。 -
positions
数组存储了每个项的top
,bottom
和height
,用于快速查找可见区域。 -
phantom.style.height
被设置为所有项的总高度,以撑开滚动条。
-
渲染可见项 (
renderVisibleItems
) :- 根据
container.scrollTop
和container.clientHeight
计算出startIndex
和endIndex
(可见项的范围,包含缓冲区)。 -
offsetY
是第一个可见项的top
值,用于通过transform: translateY()
移动content
容器,实现滚动效果,而不是移动单个列表项。 - 关键在于,
itemDiv.className
是根据itemData.isActive
动态设置的,而不是通过直接 DOM 操作添加/移除active
类。 -
itemDiv.dataset.id = itemData.id
将数据 ID 绑定到 DOM 元素上,这是事件委托中识别点击项的关键。 -
itemDiv.style.height = itemHeights.get(itemData.id) + 'px'
显式设置了每个项的高度,这对于不定高虚拟列表的布局非常重要,可以避免不必要的重排。
- 根据
-
事件委托 (
container.addEventListener('click', ...)
):-
container
是整个虚拟列表的父容器。我们只在这里添加了一个click
事件监听器。 -
event.target.closest('.list-item')
用于从事件触发的元素向上查找最近的.list-item
祖先元素。这确保即使点击的是列表项内部的子元素,也能正确识别到点击的列表项本身。 -
parseInt(clickedItem.dataset.id)
从 DOM 元素的data-id
属性中获取到对应的逻辑 ID。 -
核心逻辑:
-
data.findIndex(item => item.id === itemId)
找到数据源中对应的项。 -
const newData = [...data]; newData[itemIndex] = { ...newData[itemIndex], isActive: !newData[itemIndex].isActive };
这是更新数据源的关键。为了保持数据不可变性(在某些框架中很重要,有助于性能优化),我们创建了新的数组和新的对象来更新状态。 -
data = newData;
将更新后的数据赋值给data
。 -
renderVisibleItems();
最重要的一步:当数据更新后,我们调用渲染函数,虚拟列表会根据最新的data
重新渲染可见区域的 DOM。此时,isActive
状态的改变会反映在 DOM 元素的class
上。
-
-
总结与最佳实践
- 数据是唯一真理: 永远通过修改数据来驱动 UI 的变化,而不是直接操作 DOM。
- 事件委托: 对于列表项的交互,使用事件委托是性能和正确性的保证。
- 唯一 ID: 确保每个列表项都有一个稳定的、唯一的 ID,这是数据与 DOM 之间建立映射的基础。
- 不可变数据(可选但推荐): 在更新数据时,尽量创建新的数组或对象,而不是直接修改原有的数据结构。这有助于简化状态管理,并在使用 React/Vue 等框架时提升性能。
-
高度管理: 对于不定高虚拟列表,高度测量是核心。在数据变化可能导致高度变化时(例如,展开/收起一个项),你需要重新测量受影响项的高度,并更新
positions
数组和phantom
的高度,然后重新渲染。 - 避免副作用: 尽量将业务逻辑与渲染逻辑分离。业务逻辑负责更新数据,渲染逻辑负责根据数据渲染 UI。
- 框架优势: 如果使用 React、Vue 等现代前端框架,它们本身就提倡数据驱动和组件化,会大大简化虚拟列表的实现和旧业务逻辑的改造。它们提供了更强大的状态管理和生命周期钩子来处理复杂的交互。
通过上述方法,你可以有效地将原有直接操作 DOM 的业务逻辑改造为与不定高虚拟列表兼容的数据驱动模式。
Babel 完全上手指南:从零开始解锁现代 JavaScript 开发的超能力!
引言
Babel 是现代 JavaScript 开发中不可或缺的工具链核心,它让开发者能够使用最前沿的 JavaScript 特性,同时确保代码能在各种浏览器和环境中运行。
一、理解核心概念
1.1 什么是 Babel?
Babel 是一个 JavaScript 编译器(更准确说是转译器),主要功能包括:
- 将 ES6+ 代码转换为向后兼容的 JavaScript 版本
- 转换 JSX 语法
- 添加缺失的 polyfill
- 源代码转换(codemods)
1.2 为什么需要 Babel?
- 浏览器兼容性:不同浏览器对新特性支持不一致
- 开发效率:使用最新语法提高开发体验
- 未来兼容:提前使用尚未广泛支持的提案特性
- 生态统一:配合 TypeScript、React 等工具链
1.3 Babel 核心工作流程
- 解析(Parsing):将代码转换为抽象语法树(AST)
- 转换(Transforming):对 AST 进行各种操作
- 生成(Generating):将转换后的 AST 生成新代码
二、Babel 基础配置
2.1 基础安装
npm install --save-dev @babel/core @babel/cli @babel/preset-env
@babel/preset-env
不包含任何未进入 Stage 3 阶段的 JavaScript 语法提案,因为在 TC39 的流程中,未进入 Stage 3 阶段的提案是不会被任何浏览器所实现的。 如果确有需要,可以手动设置。通过设置shippedProposals
参数可以包含进入 Stage 3 阶段并且已经被部分浏览器实现的提案
2.2 基础配置文件 babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}
2.3 运行 Babel
npx babel src --out-dir dist
-
src
: 源文件夹 -
dist
: 输出文件夹
2.4 常用 presets
Preset | 功能 |
---|---|
@babel/preset-env |
智能转换 ES6+ 语法 |
@babel/preset-react |
转换 JSX 语法 |
@babel/preset-typescript |
转换 TypeScript |
三、常用预设@babel/preset-env
3.1 基本配置
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.32"
}
]
]
}
四、自定义转换规则
4.1 插件 vs Preset(预设)
-
Preset:插件集合(如
@babel/preset-env
包含几十个插件) -
插件:单一功能转换(如
@babel/plugin-transform-arrow-functions
)
插件和预设可以组合使用也可以各自单独使用
这意味着如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。
- 插件在 Presets 前运行。
- 插件顺序从前往后排列。
- Preset 顺序是颠倒的(从后往前)
4.2 单一插件使用示例
箭头函数插件,此插件包含于@babel/preset-env
中。
4.2.1 安装插件
npm install --save-dev @babel/plugin-transform-arrow-functions
4.2.2 在babel.config.json
中添加配置
{
"plugins": [["@babel/plugin-transform-arrow-functions", { "spec": true }]]
}
-
spec
:这个是插件编译的选项,每个插件都有自己不同的编译选项
4.2.3 具体示例
// 源文件箭头函数
[1, 2, 3].map(n => n * 2);
// { "spec": false }]
"use strict";
[1, 2, 3].map(function (n) {
return n * 2;
});
// { "spec": true }]
"use strict";
var _this = void 0;
function _newArrowCheck(n, r) { if (n !== r) throw new TypeError("Cannot instantiate an arrow function"); }
[1, 2, 3].map(function (n) {
_newArrowCheck(this, _this);
return n * 2;
}.bind(void 0));
结语
如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。
前端常说的 SCSS是个啥玩意?一篇文章给你讲的明明白白!
0.邂逅 SCSS
我们在使用脚手架 Vue cli 或者 Vite 创建 Vue 项目的时候,会发现在 css 样式标签那里有一个 lang="scss" 的属性,那 scss 是什么玩意?
专业术语:SCSS 是一种预处理器语言,是 CSS 的一种扩展。
大白话: SCSS 就像是游戏中的 Buff,因为它支持定义变量、嵌套、混合等功能,所以它会让 CSS 变得更加强大,并且更容易维护。
使用 SCSS 这玩意之后,编译后的代码还是 CSS。
1. SCSS 核心知识点
1.1 变量
作用: 主要用来存一些重复使用的值,比如尺寸、颜色等等。方便统一管理。
定义变量语法规则: 以 ** 后面不能直接跟数字。
// 定义变量
$bgColor: #a173f3;
$spaceSize: 20px;
// 使用变量
.header {
background: $bgColor;
padding: $spaceSize;
}
1.2 嵌套
作用:可以很直观的展示 html 结构层级关系,减少重复代码
.div {
ul {
li {
display: block;
a {
color: red;
&:hover { // & 表示父选择器
text-decoration: underline;
}
}
}
}
}
**注:**在 SCSS 中,& 符号用于引用父元素类名,实现嵌套规则的简化书写。例如:
.father {
&-container {
margin: 66px;
}
&-text {
font-size: 20px;
}
}
编译后对应的CSS代码为:
.father-container {
margin: 66px;
}
.father-text {
font-size: 20px;
}
1.3 混合 (Mixins)
作用:就像 js 中的函数一样,是一个可以重复利用的样式代码块,并且支持参数。
@mixin 用来定义样式代码块,@include 用来使用样式代码块。
// 定义 Mixin
@mixin border-radius($radius) {
border-radius: $radius;
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
}
// 使用 Mixin
.button {
@include border-radius(5px);
}
1.4 继承
作用:继承可以让一个选择器继承另一个选择器的样式。就是儿子继承爹的样式。
// 定义基础样式
.father-style {
color: $primary-color;
font-size: $font-size;
}
// 继承样式
.son-style {
@extend .father-style;
font-size: 20px;
}
1.5 运算
作用:在 css 中对数值进行加减乘除操作
$base-size: 10px;
.header-content {
font-size: $base-size * 3; // 30px
width: 100% - 40%; // 60%
padding: ($base-size / 5); // 2px
}
1.6 函数
作用:SCSS 提供了一些内置函数,也可以自定义函数,用来计算值。
// 使用内置函数
body {
font-size: em(16px);
}
// 自定义函数
@function doubleTwo($num) {
@return $num * 2;
}
// 使用自定义函数
.header {
height: doubleTwo(66px);
}
1.7 条件与循环
作用:动态生成样式:if判断、for循环、each 遍历。
// If 判断
@mixin myStyle($flag: false) {
@if $flag {
background: red;
color: white;
} @else {
background: green;
color: black;
}
}
// For 循环
@for $i from 1 to 10 {
.col-#{$i} {
width: 100% / $i;
}
}
// Each 遍历
$sizes: small, medium, large;
@each $size in $sizes {
.icon-#{$size} {
font-size: if($size==large, 14px,20px);
}
}
1.8 模块化
作用:就是根据一些功能拆分文件,便于统一管理和维护。 例如你可以新建一个 myVariables.scss 文件,专门用来存储变量:
// 定义变量
$primary-color: #3FAF60;
$background-color: red;
$font-size: 18px;
然后在另外一个 scss 文件中导入使用:
@import "../scss/myVariables.css";
.button {
padding: 15px;
font-size: $font-size;
background-color: $primary-color;
}
其实我们常用的做法是建一个 main.scss 文件,然后导入一些全局样式:
// 导入基础样式
@import "base/variables";
@import "base/mixins";
// 导入组件样式
@import "components/button";
@import "components/card";
// 导入布局样式
@import "layout/header";
@import "layout/footer";
@import "layout/sidebar";
// 导入页面样式
@import "pages/home";
@import "pages/about";
// 导入主题样式
@import "themes/light";
2. 在 Vue 中使用 SCSS
其实我们使用的脚手架 Vue cli 或者 Vite 本身都集成了对 scss 语法的支持,只不过我们需要在项目中安装依赖:
# Vue CLI 脚手架
npm install sass sass-loader@10 -D
# Vite 脚手架
npm install sass -D
安装依赖之后我们在 style 标签上面添加 lang="scss" 表示对 scss 的支持。另外我们通常会加上 scoped 属性表示这些样式只限制在本页面使用。
3. 答疑
3.1 sass 和 scss 什么关系?
我们在安装 scss 依赖时发现是 npm install sass。那 sass 和 scss 什么关系?
回答:
sass 和 scss 是同一种预处理器语言的不同语法版本,scss 是 Sass 的超集,兼容 sass 语法和 CSS 语法。
其实 CSS 最开始的预处理语言是 sass,后来 sass 3 版本引入了 scss 的语法扩展。说白了就是 scss 比 sass 更牛逼,更好用,但是 scss 基于 sass。所以你 npm install 的是 sass。
3.2 scss 和 css 到底什么关系
**回答:**SCSS 可以看作是增强版的 CSS,支持变量、嵌套、混合等一些非常高级的特性。
🎨前端实现文字渐变的三种方式
🎨 前言
最近开发的时候发现很多ui
图上面的标题都是带有渐变效果的,这里就记录一下前端实现文字渐变的几种方式。
完整效果如下
🎯 CSS 方式
通过给文字容器的背景设置渐变颜色,并使用background-clip
属性,将其以文字内容进行裁切。最后使用text-fill-color
属性,给文字设置透明填充来实现
属性名称 | 值 | 效果 |
---|---|---|
background | linear-gradient(to top, #b1495a, #c71a44) | 给文字容器设置渐变背景色 |
background-clip | text | 背景被裁切成文字的前景色 |
text-fill-color | transparent | 文字的填充颜色 |
效果如下
- 具体样式代码
.up-gradient {
background: linear-gradient(to top, #b1495a, #c71a44);
/* 背景被裁剪成文字的前景色。 */
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.down-gradient {
background: linear-gradient(to bottom, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.left-gradient {
background: linear-gradient(to left, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.right-gradient {
background: linear-gradient(to right, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
/* 多颜色渐变 */
.multi-gradient {
background: linear-gradient(90deg, #b1495a 10%, #c71a44 50%, #ffb86c 80%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
-
html 结构
<body> <div class="container"> <h1>CSS实现文字渐变</h1> <!-- css版本 --> <article class="panel"> <div class="panel-box-title">CSS版:</div> <div class="box"> <div class="content-text up-gradient">向上渐变</div> <div class="content-text down-gradient">向下渐变</div> <div class="content-text left-gradient">向左渐变</div> <div class="content-text right-gradient">向右渐变</div> <!-- 设置多个颜色 --> <div class="content-text multi-gradient">多颜色渐变</div> </div> </article> </div> </body>
🎨 Canvas 方式
canvas
中的文字渐变的实现方式就很简单了,因为canvas
可以直接给文字设置渐变样式。
主要用到createLinearGradient
方法,用来创建一个线性渐变,addColorStop
设置渐变的色标,就像是这个效果
最后再用fillStyle
指定使用我们创建的渐变对象即可
效果如下
核心代码
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文字渐变</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div class="container">
<!-- canvas版本 -->
<article class="panel">
<div class="panel-box-title">Canvas版:</div>
<div class="box">
<canvas id="canvas" height="180" width="900"></canvas>
</div>
</article>
</div>
</body>
<script>
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '32px Arial'
// 从左到右的渐变文字
const leftToRightGradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
leftToRightGradient.addColorStop(0, '#fff')
leftToRightGradient.addColorStop(1, '#000')
ctx.fillStyle = leftToRightGradient
ctx.fillText('Canvas 从左到右渐变', 20, 40)
// 从上到下的渐变文字
const topToBottomGradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
topToBottomGradient.addColorStop(0, '#fff')
topToBottomGradient.addColorStop(1, '#000')
ctx.fillStyle = topToBottomGradient
ctx.fillText('Canvas 从上到下渐变', 20, 80)
// 从右到左的渐变文字
const rightToLeftGradient = ctx.createLinearGradient(canvas.width, 0, 0, 0)
rightToLeftGradient.addColorStop(0, '#fff')
rightToLeftGradient.addColorStop(1, '#000')
ctx.fillStyle = rightToLeftGradient
ctx.fillText('Canvas 从右到左渐变', 20, 120)
// 从下到上的渐变文字
const bottomToTopGradient = ctx.createLinearGradient(0, canvas.height, 0, 0)
bottomToTopGradient.addColorStop(0, '#fff')
bottomToTopGradient.addColorStop(1, '#000')
ctx.fillStyle = bottomToTopGradient
ctx.fillText('Canvas 从下到上渐变', 20, 160)
</script>
</html>
🎭 SVG 方式
SVG 文字渐变的核心原理是使用 SVG 的<linearGradient>
定义渐变,然后通过fill="url(#gradientId)"
将渐变应用到文字上。
渐变效果如下
核心代码如下
<svg width="900" height="180" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 从左到右渐变 -->
<linearGradient
id="leftToRight"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从上到下渐变 -->
<linearGradient
id="topToBottom"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从右到左渐变 -->
<linearGradient
id="rightToLeft"
x1="100%"
y1="0%"
x2="0%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从下到上渐变 -->
<linearGradient
id="bottomToTop"
x1="0%"
y1="100%"
x2="0%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
</defs>
<!-- 从左到右渐变文字 -->
<text
x="20"
y="40"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#leftToRight)"
>
SVG 从左到右渐变
</text>
<!-- 从上到下渐变文字 -->
<text
x="20"
y="80"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#topToBottom)"
>
SVG 从上到下渐变
</text>
<!-- 从右到左渐变文字 -->
<text
x="20"
y="120"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#rightToLeft)"
>
SVG 从右到左渐变
</text>
<!-- 从下到上渐变文字 -->
<text
x="20"
y="160"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#bottomToTop)"
>
SVG 从下到上渐变
</text>
</svg>
📝 完整示例代码
index.css
样式代码
html,
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
color: #fff;
font-family: 'Segoe UI', 'Arial', sans-serif;
font-size: 16px;
line-height: 1.5;
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
}
/* 外层容器 */
.container {
width: 80%;
max-width: 900px;
margin: 40px auto;
padding: 24px;
background: #181c24;
border-radius: 18px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45);
border: 1.5px solid #232936;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.panel {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-box-title {
font-weight: bold;
color: #ffb86c;
text-shadow: 0 2px 8px #181c24cc;
}
/* 通用文字样式 */
.content-text {
font-size: 32px;
font-weight: bold;
}
.box {
background: #191b22;
border-radius: 14px;
padding: 24px;
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.32);
display: flex;
flex-direction: column;
border: 1.5px solid #232936;
transition: box-shadow 0.2s, border 0.2s;
position: relative;
overflow: hidden;
z-index: 1;
}
.box:hover {
box-shadow: 0 8px 48px 0 rgba(0, 0, 0, 0.76);
border: 1.5px solid #3a3f4b;
}
.up-gradient {
background: linear-gradient(to top, #b1495a, #c71a44);
/* 背景被裁剪成文字的前景色。 */
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.down-gradient {
background: linear-gradient(to bottom, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.left-gradient {
background: linear-gradient(to left, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
.right-gradient {
background: linear-gradient(to right, #b1495a, #c71a44);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
/* 多颜色渐变 */
.multi-gradient {
background: linear-gradient(90deg, #b1495a 10%, #c71a44 50%, #ffb86c 80%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 文字透明填充 */
text-fill-color: transparent;
}
index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文字渐变</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div class="container">
<h1>前端实现文字渐变的几种方式</h1>
<!-- css版本 -->
<article class="panel">
<div class="panel-box-title">CSS版:</div>
<div class="box">
<div class="content-text up-gradient">向上渐变</div>
<div class="content-text down-gradient">向下渐变</div>
<div class="content-text left-gradient">向左渐变</div>
<div class="content-text right-gradient">向右渐变</div>
<!-- 设置多个颜色 -->
<div class="content-text multi-gradient">多颜色渐变</div>
</div>
</article>
<!-- canvas版本 -->
<article class="panel">
<div class="panel-box-title">Canvas版:</div>
<div class="box">
<canvas id="canvas" height="180" width="900"></canvas>
</div>
</article>
<!-- svg版本 -->
<article class="panel">
<div class="panel-box-title">SVG版:</div>
<div class="box">
<svg width="900" height="180" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 从左到右渐变 -->
<linearGradient
id="leftToRight"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从上到下渐变 -->
<linearGradient
id="topToBottom"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从右到左渐变 -->
<linearGradient
id="rightToLeft"
x1="100%"
y1="0%"
x2="0%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
<!-- 从下到上渐变 -->
<linearGradient
id="bottomToTop"
x1="0%"
y1="100%"
x2="0%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #b1495a; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #c71a44; stop-opacity: 1"
/>
</linearGradient>
</defs>
<!-- 从左到右渐变文字 -->
<text
x="20"
y="40"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#leftToRight)"
>
SVG 从左到右渐变
</text>
<!-- 从上到下渐变文字 -->
<text
x="20"
y="80"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#topToBottom)"
>
SVG 从上到下渐变
</text>
<!-- 从右到左渐变文字 -->
<text
x="20"
y="120"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#rightToLeft)"
>
SVG 从右到左渐变
</text>
<!-- 从下到上渐变文字 -->
<text
x="20"
y="160"
font-family="Arial"
font-size="32"
font-weight="bold"
fill="url(#bottomToTop)"
>
SVG 从下到上渐变
</text>
</svg>
</div>
</article>
</div>
</body>
<script>
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '32px Arial'
// 从左到右的渐变文字
const leftToRightGradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
leftToRightGradient.addColorStop(0, '#fff')
leftToRightGradient.addColorStop(1, '#000')
ctx.fillStyle = leftToRightGradient
ctx.fillText('Canvas 从左到右渐变', 20, 40)
// 从上到下的渐变文字
const topToBottomGradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
topToBottomGradient.addColorStop(0, '#fff')
topToBottomGradient.addColorStop(1, '#000')
ctx.fillStyle = topToBottomGradient
ctx.fillText('Canvas 从上到下渐变', 20, 80)
// 从右到左的渐变文字
const rightToLeftGradient = ctx.createLinearGradient(canvas.width, 0, 0, 0)
rightToLeftGradient.addColorStop(0, '#fff')
rightToLeftGradient.addColorStop(1, '#000')
ctx.fillStyle = rightToLeftGradient
ctx.fillText('Canvas 从右到左渐变', 20, 120)
// 从下到上的渐变文字
const bottomToTopGradient = ctx.createLinearGradient(0, canvas.height, 0, 0)
bottomToTopGradient.addColorStop(0, '#fff')
bottomToTopGradient.addColorStop(1, '#000')
ctx.fillStyle = bottomToTopGradient
ctx.fillText('Canvas 从下到上渐变', 20, 160)
</script>
</html>
🎉 结尾
日常开发中还是css
版本的比较常用。另外两种,只有在特定环境下才有用。
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
2025.03.25 - 2025.07.06 更新前端面试问题总结(12道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…
目录:
中级开发者相关问题【共计 5 道题】
- 介绍一下 Web Components和Shadow DOM【热度: 489】【工程化】
- 前端倒计时有误差怎么解决【热度: 454】【web应用场景】【出题公司: 阿里巴巴】
- 让元素滚动到可视区, 有那些办法【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
- scrollIntoView、scrollIntoViewIfNeeded、scrollTo 有何区别?【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
- decodeURI 和 decodeURIComponent 有何区别【热度: 230】【web应用场景】【出题公司: 京东】
高级开发者相关问题【共计 5 道题】
- 解释 V8 引擎的内存管理机制,如何通过优化闭包或对象结构减少内存泄漏?【热度: 748】【工程化】
- Web Components 有哪些优势【热度: 489】【工程化】
- 为何 Web Components 没有成为 web 前端的主流技术框架, 反而是 react 和 vue 呢?【热度: 531】【工程化】
- 很多web前端框架里面会有约定式路由, 他们是如何实现的【热度: 331】【工程化】【出题公司: 阿里巴巴】
- 如何对一个大型 SPA(单页应用)进行全面的性能优化?请从构建阶段、运行时、网络请求、渲染等多个维度说明。【热度: 761】【工程化】【出题公司: 腾讯】
资深开发者相关问题【共计 2 道题】
-
微前端架构有哪些主流框架可以选, 各有啥优劣势?【热度: 554】【工程化】【出题公司: 腾讯】
-
如何将一个非常到大的 spa 应用, 迁移到微前端架构, 有哪些考虑因素【热度: 754】【工程化】
中级开发者相关问题【共计 5 道题】
1111. 介绍一下 Web Components和Shadow DOM【热度: 489】【工程化】
关键词:Web Components
Web Components 和 Shadow DOM 详解
一、Web Components 概述
Web Components 是一套用于构建可复用、封装性强的 Web 组件的标准,它允许开发者创建自定义 HTML 元素,这些元素具有独立的功能、样式和行为,可在不同项目中重复使用。
二、Web Components 的核心组成部分
-
Custom Elements:定义自定义 HTML 元素的 API,通过继承
HTMLElement
或其他内置元素类来创建新元素。-
示例:
class MyButton extends HTMLElement { connectedCallback() { this.innerHTML = "<button>点击我</button>"; } } customElements.define("my-button", MyButton);
-
示例:
-
HTML Templates:使用
<template>
标签定义可复用的模板,模板内容在运行时才会被解析。-
示例:
<template id="buttonTemplate"> <style> button { color: blue; } </style> <button>自定义按钮</button> </template>
-
示例:
- Shadow DOM:为组件创建独立的 DOM 树和样式作用域,避免与外部样式冲突。
- HTML Imports(已被 ES 模块取代):导入外部 HTML 文件以复用组件结构。
三、Shadow DOM 详解
Shadow DOM 是 Web Components 的关键特性,它为组件提供了封装的 DOM 环境,具有以下核心特点:
(一)Shadow DOM 的核心概念
-
Shadow Root:Shadow DOM 的根节点,通过
element.attachShadow()
方法创建。 - Light DOM:宿主元素的原始 DOM 内容。
-
Shadow DOM 与 Light DOM 的融合:通过
<slot>
元素将 Light DOM 内容插入到 Shadow DOM 中。
(二)创建 Shadow DOM 的步骤
-
创建 Shadow Root:
const shadowRoot = element.attachShadow({ mode: "open" }); // open 模式允许外部访问 shadowRoot // 或 mode: 'closed' 模式禁止外部访问
-
向 Shadow Root 中添加内容:
shadowRoot.innerHTML = ` <style>p { color: red; }</style> <p>Shadow DOM 内容</p> `;
(三)Shadow DOM 的作用
- 样式隔离:Shadow DOM 内的样式不会影响外部,外部样式也不会渗透到内部(除非使用特殊选择器)。
- 结构封装:组件内部 DOM 结构对外部不可见,避免被意外修改。
-
Slot 分发机制:通过
<slot name="xxx">
定义插槽,允许外部内容以灵活方式插入组件。
四、Web Components 与 Shadow DOM 的关系
- Shadow DOM 是 Web Components 的实现基础:通过 Shadow DOM 实现组件的样式和 DOM 封装。
-
结合使用场景:
- 创建自定义按钮、表单控件等可复用组件。
- 构建复杂页面模块(如导航栏、卡片组件),避免样式冲突。
- 封装第三方组件,防止其样式污染页面。
五、浏览器兼容性与 Polyfill
- 兼容性:现代浏览器(Chrome、Firefox、Safari 等)已广泛支持,但 IE 及旧版 Edge 不支持。
- Polyfill 库:如 lit-element、polymer 可用于兼容旧浏览器。
六、实际应用案例
-
原生组件:
<video>
、<audio>
等标签内部使用了 Shadow DOM。 - 框架实践:Vue 3 的单文件组件(.vue)、React 的 CSS-in-JS 方案借鉴了组件封装思想。
- 开源组件库:如 Material Components for the Web 基于 Web Components 构建。
七、总结
Web Components 和 Shadow DOM 是前端组件化的重要标准,通过封装性解决了传统前端开发中样式污染、代码复用性差的问题,为构建大型应用提供了更规范的解决方案。随着浏览器支持度的提升,它们已成为现代前端开发的核心技术之一。
1114. 前端倒计时有误差怎么解决【热度: 454】【web应用场景】【出题公司: 阿里巴巴】
前端倒计时出现误差是常见问题,主要由 JavaScript 的单线程特性、浏览器优化(如标签页切换时的节流)以及事件循环调度延迟引起。以下是几种解决方案:
1. 使用高精度时间戳(performance.now())
Date.now()
依赖系统时间,可能被用户或系统修改;而 performance.now()
提供更精确的时间测量,且不受系统时间影响。
function countDown(targetTime) {
const startTime = performance.now();
const totalMs = targetTime - Date.now(); // 目标时间与当前时间的差值
function update() {
const elapsedMs = performance.now() - startTime;
const remainingMs = Math.max(0, totalMs - elapsedMs);
// 更新UI
const seconds = Math.floor(remainingMs / 1000);
console.log(`剩余时间:${seconds}秒`);
if (remainingMs > 0) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
2. 定期同步服务器时间
通过 AJAX 请求定期获取服务器时间,减少累计误差:
let serverTimeOffset = 0;
// 同步服务器时间
async function syncServerTime() {
try {
const response = await fetch("/api/time"); // 后端接口返回当前时间戳
const serverTime = await response.json();
serverTimeOffset = serverTime - Date.now();
} catch (error) {
console.error("同步服务器时间失败:", error);
}
}
// 初始化同步
syncServerTime();
// 每小时同步一次
setInterval(syncServerTime, 3600000);
// 使用同步后的时间计算倒计时
function getAccurateTime() {
return Date.now() + serverTimeOffset;
}
3. 动态调整间隔
根据实际流逝时间与预期流逝时间的差值,动态调整下一次执行的延迟:
function preciseInterval(callback, delay) {
let nextTime = Date.now() + delay;
function interval() {
const currentTime = Date.now();
const drift = currentTime - nextTime; // 计算误差
callback();
nextTime += delay;
// 动态调整下一次执行时间
const nextDelay = Math.max(0, delay - drift);
setTimeout(interval, nextDelay);
}
setTimeout(interval, delay);
}
// 使用示例
preciseInterval(() => {
console.log("精确执行");
}, 1000);
4. 后台倒计时(Web Worker)
将倒计时逻辑放在 Web Worker 中,避免主线程阻塞:
// main.js
const worker = new Worker("worker.js");
worker.onmessage = (e) => {
if (e.data.type === "update") {
console.log(`剩余时间:${e.data.seconds}秒`);
}
};
// worker.js
let targetTime;
self.onmessage = (e) => {
if (e.data.type === "start") {
targetTime = e.data.targetTime;
startCountdown();
}
};
function startCountdown() {
function update() {
const remainingMs = Math.max(0, targetTime - Date.now());
const seconds = Math.floor(remainingMs / 1000);
self.postMessage({ type: "update", seconds });
if (remainingMs > 0) {
setTimeout(update, 1000);
}
}
update();
}
5. 结合 requestAnimationFrame
利用 requestAnimationFrame
的高刷新率(约 60fps)实现平滑倒计时:
function smoothCountdown(targetTime) {
function update() {
const remainingMs = Math.max(0, targetTime - Date.now());
const seconds = Math.floor(remainingMs / 1000);
// 更新UI
console.log(`剩余时间:${seconds}秒`);
if (remainingMs > 0) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
最佳实践总结
-
优先使用高精度时间:
performance.now()
比Date.now()
更可靠。 -
定期同步服务器时间:减少长时间运行的累计误差。
-
动态调整间隔:补偿 JavaScript 事件循环的延迟。
-
避免长时间 setTimeout:改用递归
setTimeout
或requestAnimationFrame
。 -
标签页可见性处理:使用
document.visibilityState
在页面不可见时暂停倒计时,可见时重新同步。
1115. 让元素滚动到可视区, 有那些办法【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
在前端开发中,使元素滚动到可视区域是常见需求。以下是几种实现方式,从简单到复杂逐步介绍:
1. Element.scrollIntoView()
这是最简单的原生方法,支持平滑滚动和对齐方式:
// 立即滚动到元素顶部与视口顶部对齐
element.scrollIntoView();
// 平滑滚动到元素底部与视口底部对齐
element.scrollIntoView({
behavior: "smooth", // 平滑滚动
block: "end", // 垂直对齐方式:start | center | end | nearest
inline: "nearest", // 水平对齐方式:start | center | end | nearest
});
优点:简单易用,兼容性好(IE11+)。
缺点:无法精确控制滚动速度或添加自定义动画。
2. Window.scrollTo() 或 window.scrollBy()
计算元素位置后滚动窗口:
// 获取元素相对于文档顶部的位置
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const targetY = rect.top + scrollTop;
// 平滑滚动到目标位置
window.scrollTo({
top: targetY,
behavior: "smooth",
});
// 或者使用 scrollBy 相对滚动
window.scrollBy({
top: rect.top, // 相对于当前位置的偏移量
behavior: "smooth",
});
优点:灵活控制目标位置。
缺点:需手动计算位置,不适合复杂布局。
3. 自定义平滑滚动动画
使用 requestAnimationFrame
实现更精细的滚动控制:
function smoothScroll(element) {
const target = element.getBoundingClientRect().top;
const duration = 500; // 动画持续时间(毫秒)
let startTime = null;
function animation(currentTime) {
if (!startTime) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
const easeProgress = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress); // 缓动函数
window.scrollTo(0, window.scrollY + target * easeProgress);
if (progress < 1) {
requestAnimationFrame(animation);
}
}
requestAnimationFrame(animation);
}
// 使用示例
smoothScroll(document.getElementById("target"));
优点:完全自定义动画效果和速度曲线。
缺点:代码复杂度较高。
4. 滚动容器内元素定位
如果元素在滚动容器内(而非整个页面),需滚动容器本身:
const container = document.getElementById("scroll-container");
const child = document.getElementById("child-element");
// 计算子元素相对于容器的位置
const containerRect = container.getBoundingClientRect();
const childRect = child.getBoundingClientRect();
const offsetTop = childRect.top - containerRect.top;
// 滚动容器
container.scrollTo({
top: container.scrollTop + offsetTop,
behavior: "smooth",
});
5. CSS Scroll Snap
使用 CSS scroll-snap-type
创建吸附效果:
.scroll-container {
scroll-snap-type: y mandatory; /* 垂直滚动,强制吸附 */
overflow-y: auto;
height: 300px; /* 容器高度 */
}
.scroll-item {
scroll-snap-align: start; /* 吸附到容器起始位置 */
height: 100%; /* 每个项目占满容器高度 */
}
<div class="scroll-container">
<div class="scroll-item">项目1</div>
<div class="scroll-item">项目2</div>
<div class="scroll-item">项目3</div>
</div>
优点:纯 CSS 实现,性能优秀。
缺点:仅控制吸附位置,无法主动触发滚动。
6. 使用第三方库
如 smooth-scroll
或 scrollreveal
:
// 安装:npm install smooth-scroll
import SmoothScroll from "smooth-scroll";
// 初始化
const scroll = new SmoothScroll('a[href*="#"]', {
speed: 500,
easing: "easeInOutCubic",
});
// 触发滚动
scroll.animateScroll(document.getElementById("target"));
选择建议
-
简单场景:优先使用
scrollIntoView()
。 -
需要自定义动画:使用
requestAnimationFrame
或第三方库。 -
容器内滚动:操作容器的
scrollTop
/scrollLeft
。 -
固定吸附点:使用 CSS
scroll-snap-type
。
无论选择哪种方式,都要考虑元素是否在视口中、滚动方向以及用户设备兼容性。
1116. scrollIntoView、scrollIntoViewIfNeeded、scrollTo 有何区别?【热度: 320】【web应用场景】【出题公司: 阿里巴巴】
scrollIntoView()
、scrollIntoViewIfNeeded()
和 scrollTo()
是 JavaScript 中用于滚动的三个方法,它们的功能和适用场景有所不同:
1. Element.scrollIntoView()
- 所属对象:DOM 元素(Element)。
- 作用:将调用该方法的元素滚动到浏览器窗口的可视区域内。
-
参数:
-
behavior
:滚动行为,可选smooth
(平滑滚动)或auto
(瞬间滚动,默认值)。 -
block
:垂直对齐方式,可选start
(元素顶部与视口顶部对齐)、center
(居中)、end
(底部对齐)或nearest
(最近边缘)。 -
inline
:水平对齐方式,可选start
、center
、end
或nearest
。
-
// 平滑滚动到元素顶部对齐
element.scrollIntoView({ behavior: "smooth", block: "start" });
- 兼容性:所有现代浏览器 + IE11。
2. Element.scrollIntoViewIfNeeded()
- 所属对象:DOM 元素(Element)。
- 作用:仅在元素当前不在可视区域内时,将其滚动到可视区域。如果元素已可见,则不执行滚动。
-
参数:
-
centerIfNeeded
:布尔值(仅 WebKit 浏览器支持,如 Chrome/Safari)。-
true
:将元素居中显示(默认值)。 -
false
:将元素滚动到最近的边缘(顶部或底部)。
-
-
// 仅在元素不可见时滚动(Chrome/Safari)
element.scrollIntoViewIfNeeded(true);
- 兼容性:Chrome、Safari 完全支持,Firefox 部分支持,IE/Edge 不支持。
3. Window.scrollTo() / Element.scrollTo()
-
所属对象:
-
window.scrollTo()
:滚动整个页面。 -
element.scrollTo()
:滚动特定容器(如<div class="scrollable">
)。
-
- 作用:滚动到指定的坐标位置。
-
参数:
-
坐标方式:
scrollTo(x, y)
,指定目标位置的绝对坐标。 -
选项对象:
-
top
:垂直滚动位置(像素)。 -
left
:水平滚动位置(像素)。 -
behavior
:滚动行为,同scrollIntoView()
。
-
-
坐标方式:
// 滚动到页面 (0, 500) 位置
window.scrollTo({ top: 500, behavior: "smooth" });
// 滚动容器内的元素
const container = document.querySelector(".scrollable");
container.scrollTo({ left: 200, behavior: "smooth" });
核心区别总结
方法 | 作用对象 | 触发条件 | 定位方式 | 兼容性 |
---|---|---|---|---|
scrollIntoView() |
元素自身 | 始终触发滚动 | 基于元素位置对齐 | 全浏览器支持 |
scrollIntoViewIfNeeded() |
元素自身 | 仅元素不可见时触发 | 自动选择最佳位置 | Chrome/Safari |
scrollTo() |
窗口或容器 | 始终触发滚动 | 指定绝对坐标 | 全浏览器支持 |
使用场景建议
-
将元素显示在视口中:用
scrollIntoView()
,适合固定导航栏跳转或表单错误定位。 -
避免不必要的滚动:用
scrollIntoViewIfNeeded()
,适合懒加载内容或动态列表。 -
精确控制滚动位置:用
scrollTo()
,适合实现进度条或分步表单。
例如:
// 平滑滚动到页面顶部
window.scrollTo({ top: 0, behavior: "smooth" });
// 将错误提示滚动到可视区
errorElement.scrollIntoView({ block: "center", behavior: "smooth" });
// 仅在图片不可见时滚动到它(Chrome/Safari)
imageElement.scrollIntoViewIfNeeded();
选择合适的方法能提升用户体验,避免不必要的页面抖动。
1117. decodeURI 和 decodeURIComponent 有何区别【热度: 230】【web应用场景】【出题公司: 京东】
decodeURI()
和 decodeURIComponent()
是 JavaScript 中用于解码 URI(统一资源标识符)的两个方法,它们的核心区别在于解码范围和适用场景。
1. 编码规则回顾
在 URI 中,某些字符(如空格、特殊符号)需要被编码为 %
后跟两位十六进制数。例如:
- 空格被编码为
%20
-
&
被编码为%26
-
#
被编码为%23
2. 核心区别
方法 | 解码范围 | 保留字符 | 典型应用场景 |
---|---|---|---|
decodeURI() |
解码整个 URI(如 http://example.com/path?query=value ) |
;/?:@&=+$,# (URI 分隔符) |
解码完整 URL |
decodeURIComponent() |
解码 URI 的组件部分(如查询参数、路径片段) | 无保留字符(解码所有 % 编码) |
解码查询参数或路径参数 |
3. 示例对比
场景 1:解码完整 URL
const encodedUrl = "http://example.com/path%2Fsubpath?param1=value1%26param2=value2";
// 使用 decodeURI()
decodeURI(encodedUrl);
// 输出:http://example.com/path/subpath?param1=value1%26param2=value2
// 注意:路径分隔符 `/`(%2F)和解码,但查询参数中的 `&`(%26)未解码
// 使用 decodeURIComponent()
decodeURIComponent(encodedUrl);
// 报错:URIError: malformed URI sequence
// 原因:完整 URL 中的分隔符(如 `?`、`&`)被错误解码
场景 2:解码查询参数
const encodedParam = "key1=value1%26key2=value2%23hash";
// 使用 decodeURI()
decodeURI(encodedParam);
// 输出:key1=value1%26key2=value2%23hash
// 注意:`&`(%26)和 `#`(%23)未被解码
// 使用 decodeURIComponent()
decodeURIComponent(encodedParam);
// 输出:key1=value1&key2=value2#hash
// 正确解码所有参数部分
4. 常见误区
-
误用
decodeURI()
处理参数:若 URL 参数包含&
、=
等符号,decodeURI()
不会解码它们,导致参数解析错误。// 错误示例:查询参数中的 `&` 未被解码 const query = "name=John%26Doe"; decodeURI(query); // "name=John%26Doe"(错误) // 正确方式 decodeURIComponent(query); // "name=John&Doe"(正确)
-
误用
decodeURIComponent()
处理完整 URL:会破坏 URL 结构(如路径分隔符被解码)。const url = "http://example.com/path%3Fparam=value"; // 假设路径中包含 `?` decodeURIComponent(url); // "http://example.com/path?param=value"(错误,路径被截断)
5. 总结
-
使用
decodeURI()
时:- 处理完整 URL(如
window.location.href
)。 - 保留 URI 中的特殊分隔符(如
?
、&
、/
)。
- 处理完整 URL(如
-
使用
decodeURIComponent()
时:- 处理 URI 的组件部分(如查询参数、路径参数)。
- 需要解码所有特殊字符(如表单提交的参数)。
口诀:
-
完整 URL →
decodeURI()
-
参数片段 →
decodeURIComponent()
高级开发者相关问题【共计 5 道题】
1110. 解释 V8 引擎的内存管理机制,如何通过优化闭包或对象结构减少内存泄漏?【热度: 748】【工程化】
关键词:内存机制、内存泄露
关键词:内存机制、内存泄露
关键词:内存机制、内存泄露
一、V8 引擎内存管理机制概述
V8 是 Google 开发的 JavaScript 引擎,采用自动垃圾回收机制管理内存,其核心流程包括:
1. 内存分配
-
栈内存:存储原始类型值(如
Number
、String
、Boolean
)和函数调用栈,由引擎自动分配/释放。 -
堆内存:存储引用类型值(如
Object
、Array
、Function
),需手动分配(通过new
等操作),由垃圾回收器自动回收。
2. 垃圾回收(GC)机制
V8 使用分代回收策略,将堆内存分为新生代和老生代,针对不同生命周期的对象采用不同回收算法:
-
新生代(小内存空间,存活时间短):
-
算法:
Scavenge
(复制算法)。 -
流程:将内存分为
From
和To
两个区域,存活对象从From
复制到To
,清空From
并交换区域角色。 - 适用场景:临时变量、函数作用域内的对象。
-
算法:
-
老生代(大内存空间,存活时间长):
-
算法:
Mark-Sweep
(标记-清除)和Mark-Compact
(标记-整理)结合。 -
流程:
- 标记:遍历所有可达对象并标记为存活。
- 清除:删除未标记的对象,回收内存。
- 整理:移动存活对象,压缩内存空间,避免碎片。
- 适用场景:全局对象、闭包引用的对象。
-
算法:
二、内存泄漏的常见原因
内存泄漏指不再使用的对象因被错误引用而无法被 GC 回收,常见场景包括:
- 闭包不当使用:内部函数引用外部变量,导致变量无法释放。
- 全局变量泄漏:意外创建全局变量(如未声明直接赋值)。
-
DOM 引用泄漏:DOM 对象与 JavaScript 对象形成循环引用(如
element.onclick = element
)。 -
定时器未清除:
setInterval
/setTimeout
创建的回调函数未及时取消。 -
循环引用:对象间相互引用(如
obj.a = obj.b; obj.b = obj.a
)。
三、通过优化闭包减少内存泄漏
1. 避免不必要的闭包
-
问题:嵌套函数过度引用外部作用域变量,导致变量常驻堆内存。
function outer() { const largeData = new Array(1000000).fill(1); // 大数组 function inner() { // 仅使用部分数据时,仍引用整个 largeData return largeData.slice(0, 10); } return inner; // 闭包持有 largeData 引用 } const fn = outer(); // largeData 无法释放
-
优化:仅传递闭包需要的变量,避免引用整个对象。
function outer() { const largeData = new Array(1000000).fill(1); const neededData = largeData.slice(0, 10); // 提取必要数据 function inner() { return neededData; // 闭包仅引用 small data } return inner; }
2. 及时释放闭包引用
-
问题:闭包引用的变量在不再使用时未被解除引用。
let globalFn = null; function createClosure() { const obj = { key: "value" }; globalFn = function () { return obj; // 闭包引用 obj }; } createClosure(); // 后续不再需要 globalFn 时,未置为 null
-
优化:不再使用闭包时,手动解除引用。
let globalFn = null; function createClosure() { const obj = { key: "value" }; globalFn = function () { return obj; }; } createClosure(); // 释放闭包 globalFn = null; // obj 失去引用,可被 GC 回收
3. 使用弱引用(WeakMap/WeakSet)
-
场景:闭包需缓存对象,但不希望阻止其回收。
const cache = new WeakMap(); // 弱引用 map function outer(obj) { cache.set(obj, function () { // 闭包引用 obj,但 WeakMap 不阻止 obj 被回收 return obj.property; }); return cache.get(obj); }
-
原理:
WeakMap
的键为弱引用,若对象无其他引用则会被回收,闭包自动失效。
四、通过优化对象结构减少内存泄漏
1. 避免循环引用
-
问题:对象间相互引用导致 GC 无法回收。
function createCycle() { const a = { name: "a" }; const b = { name: "b" }; a.ref = b; // a 引用 b b.ref = a; // b 引用 a(循环引用) } createCycle(); // a 和 b 无法被回收
-
优化:手动断开循环引用。
function createCycle() { const a = { name: "a" }; const b = { name: "b" }; a.ref = b; b.ref = a; // 使用完毕后断开引用 a.ref = null; b.ref = null; }
2. 减少不必要的属性引用
-
问题:对象属性引用大型数据或全局对象。
const globalData = { largeArray: new Array(1000000).fill(1) }; function createObject() { return { data: globalData, // 引用全局大型对象 method: function () { /* 使用 data */ }, }; } const obj = createObject(); // 即使不再使用 obj.data,globalData 仍被引用
-
优化:仅在需要时传递数据副本或弱引用。
const globalData = { largeArray: new Array(1000000).fill(1) }; function createObject() { // 传递副本而非原对象(适用于不可变数据) return { data: { ...globalData }, // 浅拷贝,减少引用 method: function () { /* 使用 data */ }, }; }
3. 合理使用对象池(Object Pooling)
-
场景:频繁创建/销毁大型对象时,复用对象可减少内存分配/回收压力。
const objectPool = []; function createObject() { if (objectPool.length > 0) { return objectPool.pop(); // 复用池中的对象 } return { data: new Array(1000000).fill(1) }; // 新建对象 } function destroyObject(obj) { obj.data.length = 0; // 清理数据 objectPool.push(obj); // 放回对象池 }
- 注意:对象池需配合引用计数或手动管理,避免无效对象残留。
五、内存泄漏检测工具
-
Chrome DevTools:
- Memory 面板:录制内存快照,对比不同时刻的对象引用,定位泄漏对象。
- Performance 面板:分析内存分配趋势,识别频繁创建的未释放对象。
-
Node.js 工具:
-
process.memoryUsage()
:监控堆内存使用情况。 -
--expose-gc
标志:手动触发 GC,配合console.log
调试。
-
总结
优化内存管理的核心原则是:减少不必要的引用,及时释放不再使用的对象。通过合理设计闭包作用域、避免循环引用、使用弱引用和对象池等策略,可有效降低内存泄漏风险。同时,结合浏览器或 Node.js 提供的调试工具,定期分析内存快照,是定位和解决泄漏问题的关键。
1112. Web Components 有哪些优势【热度: 489】【工程化】
关键词:Web Components
Web Components 作为现代前端开发的重要技术,具有以下显著优势:
一、真正的组件封装
-
样式隔离
Shadow DOM 确保组件内部样式不会泄露到外部,也不受外部样式影响,彻底解决 CSS 全局污染问题。
示例:组件内部的.button { color: red }
不会影响外部按钮样式。 -
DOM 封装
组件内部结构对外部不可见,避免被意外修改,实现真正的关注点分离。
对比:传统组件(如 React/Vue)仍依赖全局 DOM 结构。
二、原生浏览器支持
-
无需框架依赖
作为浏览器原生标准(如 Chrome、Firefox、Safari 均支持),可直接在任何环境使用,降低技术栈复杂度。
场景:在 legacy 项目或多框架共存环境中复用组件。 -
轻量级
相比框架组件(如 React 组件需引入 React 库),Web Components 更轻量,适合性能敏感场景。
三、跨框架兼容性
-
真正的“一次编写,到处运行”
可在 React、Vue、Angular 等任何框架中无缝集成,甚至可用于无框架的原生项目。
示例:<!-- 在 Vue 项目中使用 Web Components --> <custom-button @click="handleClick"></custom-button>
四、高度可复用性
-
标准化组件格式
基于 HTML、CSS、JS 标准,无需学习特定框架语法,降低开发者学习成本。
生态:可复用现有 HTML 组件生态(如 Material Design Web Components)。 -
独立分发
可打包为独立文件(如.js
),通过 CDN 直接引入,无需复杂构建流程。
示例:<script src="https://cdn.example.com/custom-button.js"></script>
五、渐进式增强友好
-
支持降级体验
组件可先提供基础功能(Light DOM),再通过 JS 增强(Shadow DOM),确保低 JS 环境下仍可用。
示例:<custom-form> <form> <!-- 基础表单内容 --> </form> </custom-form>
六、未来兼容性
-
W3C 标准演进
作为浏览器原生标准,长期维护性更强,减少技术栈过时风险。
对比:第三方框架(如 jQuery、Backbone)可能随时间淘汰。
七、性能优化
-
浏览器级优化
原生组件渲染效率更高,尤其在大规模列表渲染时(如 1000+ 组件),性能优于虚拟 DOM 框架。 -
按需加载
通过<script type="module">
和动态导入,可实现组件的懒加载。
示例:import("./heavy-component.js").then(() => { document.body.innerHTML += "<heavy-component></heavy-component>"; });
八、简化团队协作
-
标准化接口
通过自定义属性(Attributes)和事件(Events)定义清晰的组件接口,降低团队沟通成本。
示例:<custom-slider min="0" max="100" value="50" @change="updateValue"></custom-slider>
九、与现有技术互补
-
框架集成
主流框架(如 React、Vue)均提供官方支持 Web Components 的方式。
React 示例:function App() { return <custom-element some-prop="value" />; }
-
微前端场景
作为微前端架构中的“原子组件”,实现跨应用复用。
十、降低技术债务
-
独立升级
组件可独立于应用升级,无需重构整个项目。
场景:将 legacy 项目逐步迁移至现代架构。
应用场景举例
- 企业级组件库(如 Ant Design、Element UI 的 Web Components 版本)
- 跨部门复用组件(如 Header、Footer、Toast 等基础组件)
- 第三方插件集成(如广告组件、评论系统)
- 低代码平台(通过标准组件降低用户学习成本)
总结
Web Components 凭借原生支持、真正封装、跨框架兼容三大核心优势,成为构建未来前端应用的理想选择。尤其适合需要长期维护、多团队协作、跨技术栈集成的大型项目。随着浏览器兼容性的提升(当前支持率约 95%),其应用场景将越来越广泛。
1113. 为何 Web Components 没有成为 web 前端的主流技术框架, 反而是 react 和 vue 呢?【热度: 531】【工程化】
关键词:Web Components
Web Components 虽具备技术优势,但未能成为前端主流框架的核心原因可从以下维度分析:
一、生态与工具链成熟度
-
React/Vue 的生态优势
主流框架拥有完善的工具链(如 Webpack、Vite)、状态管理库(Redux、Pinia)、UI 组件库(Ant Design、Element),以及丰富的文档和社区支持。
对比:Web Components 的生态碎片化严重,缺乏统一的最佳实践。 -
框架集成成本
在已有项目中引入 Web Components 可能需修改构建流程,而 React/Vue 可无缝集成现有工具链。
示例:Vue 组件可直接使用<script setup>
语法,Web Components 则需手动处理生命周期。
二、开发体验与抽象层级
-
声明式 vs 命令式
React/Vue 通过 JSX/模板语法提供更高级的抽象,降低 DOM 操作复杂度。
示例:React 的useState
钩子比 Web Components 的attributeChangedCallback
更直观。 -
状态管理复杂度
Web Components 原生未提供状态管理方案,处理复杂数据流需自行实现或引入第三方库(如 Redux),而 React/Vue 内置状态管理机制。
三、学习曲线与开发者偏好
-
入门门槛
React/Vue 的概念(如虚拟 DOM、组件化)更贴近现代前端思维,而 Web Components 需掌握 Shadow DOM、Custom Elements 等多个低阶 API。
数据:Stack Overflow 调查显示,React/Vue 的问题活跃度远高于 Web Components。 -
框架黏性
开发者倾向使用已熟悉的框架(如 React 开发者更愿用 React 生态组件),而非切换技术栈。
四、性能与优化难度
-
虚拟 DOM 的优势
React/Vue 通过虚拟 DOM 差异更新减少真实 DOM 操作,在复杂 UI 场景下性能更优。
测试:大型列表渲染中,React 的shouldComponentUpdate
比 Web Components 的原生更新更高效。 -
优化工具缺失
React/Vue 提供 Hooks、Suspense 等优化工具,Web Components 需手动实现类似功能。
五、浏览器兼容性与 Polyfill
-
兼容性成本
Web Components 在旧版浏览器(如 IE11)需引入大型 Polyfill,导致包体积膨胀。
数据:核心 Polyfill 约增加 100KB 体积,而 React 压缩后约 42KB。 -
特性碎片化
不同浏览器对 Shadow DOM 的实现存在细微差异(如 CSS 变量支持),增加测试成本。
六、框架厂商推动与社区效应
-
商业公司背书
React(Meta)、Vue(开源但有官方支持)受益于大厂资源投入,持续迭代并推广新特性。
对比:Web Components 作为 W3C 标准,缺乏单一实体推动生态建设。 -
社区驱动创新
React/Vue 的第三方插件(如 React Query、Vue Router)加速了其普及,而 Web Components 社区活跃度较低。
七、适用场景差异
-
Web Components 的定位
更适合构建原子组件(如按钮、输入框)或跨框架共享组件,而非完整应用框架。
案例:Google 的 Material Web Components 作为独立组件库使用。 -
框架的端到端解决方案
React/Vue 提供路由、状态管理、构建工具等一站式解决方案,降低全栈开发复杂度。
八、历史机遇与开发者惯性
-
时间窗口错位
React(2013)、Vue(2014)诞生于前端工程化需求爆发期,而 Web Components 标准成熟较晚(2019 年全面支持)。 -
技术选型惯性
企业更倾向选择经过验证的框架(如 React),而非实验性技术。
九、设计哲学差异
-
标准化 vs 灵活性
Web Components 追求标准化,而 React/Vue 允许灵活选择技术栈(如状态管理方案)。
争议:Web Components 的 Shadow DOM 完全隔离样式,导致某些场景(如全局主题)难以实现。
十、未来趋势:互补而非替代
-
混合架构兴起
现代项目常采用“框架 + Web Components”的混合模式(如在 React 中使用 Web Components 实现跨团队组件)。 -
框架原生支持
React 18+、Vue 3 均提供更好的 Web Components 集成方案,表明两者正走向融合而非竞争。
总结
Web Components 未成为主流的核心矛盾在于:标准化的底层技术 vs 开发者对高效工具链和抽象层级的需求。未来更可能作为基础设施(如组件通信标准)而非独立框架存在,与 React、Vue 等上层框架形成互补生态。
1118. 很多web前端框架里面会有约定式路由, 他们是如何实现的【热度: 331】【工程化】【出题公司: 阿里巴巴】
关键词:约定是路由
约定式路由(Convention over Configuration,CoC)是现代前端框架(如 Next.js、Nuxt.js、VitePress 等)广泛采用的路由实现方式,其核心思想是根据文件目录结构自动生成路由配置,无需手动编写冗长的路由表。下面介绍其实现原理和关键技术:
一、核心原理
约定式路由通过以下步骤工作:
-
文件系统扫描:框架在构建或运行时遍历指定目录(如
pages/
),获取所有文件和文件夹结构。 -
路径映射规则:将文件路径转换为路由路径,例如:
-
pages/index.js
→/
-
pages/posts/[id].js
→/posts/:id
(动态路由)
-
- 路由配置生成:根据映射规则生成路由配置对象(如 React Router 或 Vue Router 所需的格式)。
- 运行时匹配:在用户访问时,根据 URL 匹配对应的组件。
二、关键实现细节
1. 文件系统扫描与路径解析
框架使用 Node.js 的fs
模块读取文件目录,并递归生成路径树。例如:
// 简化的文件扫描逻辑
import fs from "fs";
import path from "path";
function scanPages(dir, basePath = "") {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const routes = [];
for (const entry of entries) {
const filePath = path.join(dir, entry.name);
const routePath = path.join(basePath, entry.name);
if (entry.isDirectory()) {
// 递归扫描子目录
routes.push(...scanPages(filePath, routePath));
} else {
// 处理文件(如.js、.vue)
routes.push({
file: filePath,
path: convertToRoutePath(routePath), // 转换为路由路径
});
}
}
return routes;
}
// 路径转换示例:pages/posts/[id].js → /posts/:id
function convertToRoutePath(filePath) {
// 移除扩展名
let route = filePath.replace(/\.(js|jsx|ts|tsx|vue)$/, "");
// 处理动态路由:[id] → :id
route = route.replace(/\[([^\]]+)\]/g, ":$1");
// 处理索引文件:index → /
route = route.replace(/\/index$/, "");
// 确保以斜杠开头
return route.startsWith("/") ? route : `/${route}`;
}
2. 动态路由与嵌套路由
-
动态路由:使用方括号
[]
表示参数,例如:-
pages/users/[id].js
→ 匹配/users/123
-
pages/[...all].js
→ 匹配所有路径(通配符路由)
-
-
嵌套路由:通过目录结构实现,例如:
pages/ posts/ index.js → /posts [id]/ index.js → /posts/:id comments/ index.js → /posts/:id/comments
3. 路由配置生成
将扫描结果转换为框架所需的路由配置格式。例如,为 React Router 生成配置:
// 生成React Router配置
function generateReactRoutes(pages) {
return pages.map((page) => ({
path: page.path,
element: () => import(`./pages/${page.file}`), // 动态导入组件
}));
}
// 使用生成的路由配置
const router = createBrowserRouter(generateReactRoutes(pages));
4. 特殊文件处理
-
布局文件:如
_layout.js
或layout.vue
,用于包裹子路由:pages/ _layout.js → 所有页面共用布局 index.js → 使用_layout.js的布局
-
错误页面:如
404.js
或error.vue
,处理未匹配的路由:// 404页面自动映射到未匹配的路由 { path: '*', element: <NotFoundPage /> }
5. 运行时优化
-
按需加载:使用动态导入(
import()
)实现组件懒加载。 -
路由预取:在用户可能访问的链接上预加载组件(如 Next.js 的
next/link
)。 - 缓存机制:开发环境中缓存扫描结果,仅在文件变化时重新生成路由。
三、不同框架的实现差异
框架 | 约定规则 | 实现特点 |
---|---|---|
Next.js |
pages/ 目录,支持[param] 动态路由 |
服务端渲染(SSR)支持、自动代码分割 |
Nuxt.js |
pages/ 目录,支持_param 动态路由 |
基于 Vue Router,支持中间件 |
VitePress |
docs/ 目录,Markdown 文件自动转换 |
静态网站生成(SSG),支持 Vue 组件 |
四、优缺点
优点
- 减少样板代码:无需手动维护路由配置。
- 提高一致性:文件结构即路由结构,直观易懂。
- 易于扩展:新增页面只需添加文件,无需修改路由表。
缺点
- 灵活性受限:复杂路由模式可能需要额外配置。
- 学习成本:需要熟悉框架的约定规则。
- 性能开销:大型项目中扫描文件系统可能影响构建速度。
五、手动实现简易版约定式路由
以下是一个简化的实现示例,用于理解核心逻辑:
// 简易约定式路由实现
import fs from "fs";
import path from "path";
import { createRouter, createWebHistory } from "vue-router";
// 扫描pages目录
const pagesDir = path.resolve(__dirname, "pages");
const routes = fs
.readdirSync(pagesDir)
.filter((file) => file.endsWith(".vue"))
.map((file) => {
const name = file.replace(/\.vue$/, "");
const path = name === "index" ? "/" : `/${name}`;
return {
path,
component: () => import(`./pages/${file}`),
};
});
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
六、总结
约定式路由通过文件系统映射和自动化配置,极大简化了路由管理。其核心在于扫描文件、转换路径、生成配置和运行时匹配。现代框架在此基础上添加了动态路由、嵌套路由、懒加载等高级特性,提升了开发体验和应用性能。
1119. 如何对一个大型 SPA(单页应用)进行全面的性能优化?请从构建阶段、运行时、网络请求、渲染等多个维度说明。【热度: 761】【工程化】【出题公司: 腾讯】
关键词:性能优化
对大型 SPA(单页应用)进行全面性能优化需要从多个维度入手,以下是系统性的优化策略:
一、构建阶段优化
1. 代码分割与懒加载
-
动态导入(Dynamic Import):按需加载路由组件、组件库、第三方模块
// React示例:懒加载路由组件 const HomePage = React.lazy(() => import("./pages/HomePage")); // Vue示例:异步组件 const HomePage = () => import("./pages/HomePage.vue");
-
路由级分割:按路由拆分 chunks,减少首屏加载体积
-
组件级分割:对大型组件(如数据表格、图表)单独拆分
2. Tree Shaking
- 启用 ESModule + 生产环境配置,移除未使用的代码
- 优化第三方库:选择支持 Tree Shaking 的库(如 Lodash-es)
3. 压缩与混淆
- 使用 Terser 压缩 JS,cssnano 压缩 CSS
- 移除调试代码:
console.log
、debugger
4. 资源预加载/预取
- 通过 HTML 标签声明预加载关键资源
<link rel="preload" href="critical.js" as="script" /> <link rel="prefetch" href="non-critical.js" as="script" />
- 框架集成:如 Next.js 的
next/link
自动预取
二、运行时优化
1. 虚拟列表(Virtual List)
- 只渲染可视区域内的列表项,大幅减少 DOM 节点数量
- 库推荐:
react-window
(React)、vue-virtual-scroller
(Vue)
2. 防抖(Debounce)与节流(Throttle)
- 高频事件(如滚动、窗口 resize)处理优化
// 防抖示例 const debouncedHandleScroll = debounce(handleScroll, 300); window.addEventListener("scroll", debouncedHandleScroll);
3. 状态管理优化
- 避免全局状态滥用,使用局部状态(如 React 的 useState)
- 不可变数据结构:使用 Immer 简化不可变数据操作
- 状态分片:按功能拆分 store(如 Redux Toolkit 的 slice)
4. 内存管理
- 避免内存泄漏:及时清理定时器、事件监听器
- 使用 WeakMap/WeakSet 存储临时引用
三、网络请求优化
1. 缓存策略
- HTTP 缓存:合理设置
Cache-Control
、ETag
- 客户端缓存:使用
localStorage
、IndexedDB
缓存静态数据 - Service Worker:实现离线缓存和请求拦截
2. 资源加载优化
- 图片优化:使用 WebP/AVIF 格式、响应式图片(srcset)
<img src="image.jpg" srcset="image.jpg 1x, image@2x.jpg 2x" loading="lazy" alt="Description" />
- 按需加载字体:使用
font-display: swap
避免 FOUT
3. API 请求优化
- 合并请求:将多个小请求合并为批量请求
- 缓存失效策略:使用 SWR(Stale-While-Revalidate)模式
- 服务端数据预取:如 Next.js 的
getServerSideProps
四、渲染优化
1. 减少重排(Layout)与重绘(Paint)
- 批量修改 DOM:使用 DocumentFragment
- 避免强制同步布局(Force Synchronous Layout)
- 使用
transform
和opacity
进行动画,利用合成层(Compositing)
2. 懒加载(Lazy Loading)
- 图片懒加载:浏览器原生支持(
loading="lazy"
) - 组件懒加载:结合 Intersection Observer API 实现可视区域加载
3. 服务端渲染(SSR)/静态站点生成(SSG)
- 首屏 HTML 直出,减少客户端渲染时间
- 框架支持:Next.js(React)、Nuxt.js(Vue)
4. 减少包体积
- 移除不必要的依赖
- 使用 CDN 加载第三方库:
<script src="https://cdn.tailwindcss.com"></script>
五、工具与监控
1. 性能分析工具
- Chrome DevTools:Lighthouse、Performance 面板
- WebPageTest:多地点性能测试
- 框架专用工具:React DevTools 的 Profiler
2. 持续监控
- 埋点:记录关键指标(FP、FCP、LCP、TTFB)
- 告警:设置性能阈值,异常时自动通知
六、框架特定优化
React
- 使用
React.memo
、useMemo
、useCallback
避免不必要渲染 - 使用 Concurrent Mode(并发模式)提高响应性
Vue
- 使用
v-once
渲染静态内容 - 使用
v-memo
缓存组件树
七、总结
大型 SPA 性能优化需遵循以下原则:
-
先测量,后优化:使用工具定位瓶颈点
-
从大到小:优先处理首屏加载、关键路径
-
分层优化:构建、网络、运行时、渲染各维度协同
-
持续监控:建立性能基线,防止退化
资深开发者相关问题【共计 2 道题】
1120. 微前端架构有哪些主流框架可以选, 各有啥优劣势?【热度: 554】【工程化】【出题公司: 腾讯】
关键词:微前端框架
以下是前端领域主流微前端框架的深度解析,涵盖核心特性、优劣势及适用场景,结合最新技术动态和企业级实践:
一、核心框架对比与选型指南
1. Qiankun(蚂蚁集团)
-
核心特性:
- 基于 Single-SPA 封装,支持 React/Vue/Angular 等多框架共存
- 提供 JS 沙箱(Proxy/快照机制)和 CSS 沙箱(Shadow DOM/动态作用域)
- 完整的生命周期管理和路由劫持机制
-
优势:
- 成熟稳定,经过蚂蚁金服大规模生产验证
- 开箱即用,配置简单,适合快速搭建微前端架构
- 完善的生态支持(如 Vue CLI 插件、Webpack 工具链)
-
劣势:
- 对子应用侵入性较强,需改造入口文件和构建配置
- 沙箱机制在复杂场景下可能存在性能损耗
-
适用场景:
- 大型企业级应用,技术栈多样且需长期维护
- 需统一路由管理和状态共享的中台项目
2. Single-SPA(独立开源)
-
核心特性:
- 微前端底层框架,提供应用加载、路由调度和生命周期管理
- 高度灵活,可与任意框架结合
- 支持应用预加载和资源共享
-
优势:
- 无框架绑定,适合自定义程度高的复杂场景
- 社区活跃,插件生态丰富(如 single-spa-react/single-spa-vue)
-
劣势:
- 配置繁琐,需手动处理样式隔离和通信机制
- 对新手不够友好,学习曲线陡峭
-
适用场景:
- 技术栈混合且需高度定制的项目
- 已有成熟路由和状态管理体系的应用改造
3. Module Federation(Webpack 5 原生功能)
-
核心特性:
- 基于 Webpack 5 的模块共享机制,支持跨应用动态加载模块
- 天然支持依赖共享,减少重复打包
- 与 Webpack 深度集成,开发体验一致
-
优势:
- 模块级共享,代码复用率高
- 按需加载提升性能,适合大型应用
-
劣势:
- 强依赖 Webpack 5,构建配置复杂
- 缺乏完整的微前端生态(如路由、通信需自行实现)
-
适用场景:
- 技术栈统一且使用 Webpack 5 的项目
- 需要共享组件库或基础模块的多团队协作
4. MicroApp(京东)
-
核心特性:
- 基于类 Web Components 实现,子应用零改造接入
- 提供 JS 沙箱(Proxy)和样式隔离(Shadow DOM)
- 支持虚拟路由系统和跨框架通信
-
优势:
- 低侵入性,子应用只需配置跨域即可接入
- 高性能沙箱机制,支持多实例场景
-
劣势:
- 生态较小,社区支持有限
- Proxy 沙箱在 IE 等旧浏览器中不兼容
-
适用场景:
- 快速集成现有项目,尤其是技术栈混杂的遗留系统
- 对性能和隔离性要求较高的中大型应用
5. Wujie(腾讯)
-
核心特性:
- 结合 Web Components 和 iframe,实现原生隔离
- 支持样式、JS、路由全隔离,安全性高
- 提供轻量级通信机制(postMessage/自定义事件)
-
优势:
- 天然隔离性,适合金融、医疗等高安全场景
- 高性能按需加载,首屏时间优化显著
-
劣势:
- iframe 的历史包袱(如滚动条、SEO 问题)
- Web Components 兼容性问题(IE11 不支持)
-
适用场景:
- 对隔离性和安全性要求极高的场景
- 技术栈统一且现代浏览器占比高的项目
6. Garfish(字节跳动)
-
核心特性:
- 基于 Proxy 沙箱和动态样式隔离,支持多实例
- 提供跨框架通信和状态管理工具链
- 集成 Vite 和 Webpack,构建灵活
-
优势:
- 高效资源管理,支持并行加载和缓存优化
- 强大的扩展性,适合复杂前端生态
-
劣势:
- 文档和社区活跃度待提升
- 对构建工具链的整合需一定学习成本
-
适用场景:
- 大型应用跨团队协作,需高效资源调度
- 技术栈混合且追求性能的互联网产品
7. ICestark(阿里巴巴)
-
核心特性:
- 基于 qiankun 和 Web Components,支持多端(Web/小程序)
- 强调状态管理和模块化,提供全局状态总线
- 支持服务端渲染(SSR)和静态站点生成(SSG)
-
优势:
- 企业级解决方案,适合复杂业务场景
- 完善的状态管理和跨应用通信机制
-
劣势:
- 配置复杂,学习曲线陡峭
- 对 SSR 和 SSG 的支持需额外配置
-
适用场景:
- 大型企业级多端应用,需统一状态管理
- 对 SSR 和 SEO 有强需求的项目
8. Piral(独立开源)
-
核心特性:
- 基于插件机制,支持动态加载微应用模块
- 提供可视化插件市场和 CLI 工具链
- 支持混合技术栈和渐进式集成
-
优势:
- 高度可扩展,适合插件化开发模式
- 低侵入性,子应用可独立开发和部署
-
劣势:
- 生态较小,中文资料较少
- 对复杂路由和状态管理支持较弱
-
适用场景:
- 插件化架构和快速迭代的创新项目
- 团队熟悉 React 或 Vue 的中小型应用
二、关键维度对比与选型建议
1. 技术栈兼容性
- 多框架支持:Qiankun > Single-SPA > Garfish > ICestark
- 技术栈无关:MicroApp > Wujie > Module Federation
- 推荐场景:若存在 React/Vue/Angular 混合开发,优先选择 Qiankun 或 Single-SPA;若需完全技术栈无关,MicroApp 和 Wujie 更优。
2. 隔离性与安全性
- 强隔离:Wujie(iframe+Shadow DOM)> MicroApp(Proxy 沙箱)> Qiankun(Proxy/快照沙箱)
- 弱隔离:Module Federation(依赖 Webpack 模块作用域)
- 推荐场景:金融、医疗等高安全场景选择 Wujie;普通业务场景 Qiankun 或 MicroApp 即可。
3. 性能与资源管理
- 高性能:Module Federation(模块级按需加载)> Garfish(并行加载优化)> MicroApp(轻量级沙箱)
- 低性能:Qiankun(沙箱开销)> Single-SPA(手动优化要求高)
- 推荐场景:追求极致性能选择 Module Federation 或 Garfish;中大型应用可平衡 Qiankun 的成熟度与性能。
4. 开发体验与学习成本
- 低学习成本:Qiankun > MicroApp > Wujie
- 高学习成本:Module Federation > Single-SPA > ICestark
- 推荐场景:新手或快速交付项目选择 Qiankun 或 MicroApp;复杂场景需深入学习 Single-SPA 或 Module Federation。
5. 生态与社区支持
- 成熟生态:Qiankun > Single-SPA > Module Federation
- 新兴生态:MicroApp > Wujie > Garfish
- 推荐场景:长期维护项目选择 Qiankun 或 Single-SPA;创新项目可尝试 MicroApp 或 Garfish。
三、落地避坑指南
1. 样式隔离方案选择
- Shadow DOM:适合现代浏览器环境,需处理弹窗组件挂载问题
- 动态样式作用域:兼容性好,需监控动态插入样式
-
推荐实践:默认启用 Qiankun 的
experimentalStyleIsolation
,关键子应用逐步迁移至 Shadow DOM
2. 通信机制设计
-
轻量级通信:使用框架内置事件总线(如 Qiankun 的
props
传递) - 复杂通信:结合状态管理库(如 Redux)或微服务 API
- 避坑点:避免直接操作全局变量,优先使用框架提供的通信接口
3. 路由管理策略
- 主应用统一管理:适合单页应用模式,需处理子应用路由前缀
- 子应用自治:适合多页应用模式,需注意路由冲突
-
推荐实践:使用 Qiankun 的
activeRule
或 Single-SPA 的registerApplication
配置路由匹配规则
4. 资源加载优化
-
预加载:Qiankun 的
preload
配置或 Webpack 的prefetch
注释 - 按需加载:Module Federation 的动态导入或 Garfish 的并行加载机制
- 避坑点:避免同时加载过多子应用,优先加载关键路径资源
四、总结与趋势展望
1. 框架选型决策树
- 技术栈多样 → Qiankun 或 Single-SPA
- 高隔离需求 → Wujie 或 MicroApp
- 模块共享优先 → Module Federation 或 EMP
- 快速集成 → MicroApp 或 Piral
- 企业级复杂场景 → ICestark 或 Garfish
2. 未来趋势
- Web Components 普及:Wujie、MicroApp 等框架将更受青睐
- 构建工具链整合:Vite+Module Federation 模式(如 EMP)可能成为主流
- 全栈微前端:ICestark 等框架向多端(Web/小程序/Node)扩展
通过综合评估项目需求、技术栈现状和团队能力,选择最适合的微前端框架,并结合上述避坑指南,可有效降低集成成本,提升系统可维护性和扩展性。
1121. 如何将一个非常到大的 spa 应用, 迁移到微前端架构, 有哪些考虑因素【热度: 754】【工程化】
关键词:微前端框架
将大型 SPA(单页应用)迁移到微前端架构是一个复杂的系统工程,需要从业务、技术、团队等多维度综合考量。其核心目标是解决大型 SPA 的代码臃肿、团队协作低效、技术栈锁定、部署缓慢等问题,同时确保迁移过程平稳、业务不受影响。
一、迁移前的核心前提:明确目标与现状评估
在动手迁移前,需先明确“为什么要做微前端”,避免为了技术而技术。同时,需全面评估现有 SPA 的现状,为迁移策略提供依据。
1. 明确迁移目标与价值
微前端的核心价值是**“去中心化的前端架构”**,迁移目标应围绕以下几点展开:
- 团队自治:让不同团队(如商品、订单、支付团队)独立开发、测试、部署各自负责的模块,减少跨团队协作成本。
- 技术栈灵活:允许不同微应用使用不同技术栈(如老模块用 Vue2,新模块用 React),避免技术栈锁定,支持增量升级。
- 独立部署:单个微应用的更新无需全量发布整个应用,缩短发布周期,降低部署风险。
- 故障隔离:单个微应用崩溃不影响其他模块,提高系统稳定性。
若现有 SPA 未遇到上述问题(如团队小、业务简单),则无需迁移。
2. 评估现有 SPA 的现状
需深入分析现有应用的“痛点”和“基础”,避免盲目迁移:
- 代码结构:是否有清晰的业务模块边界?模块间耦合度如何(如是否大量使用全局变量、公共函数)?是否存在“牵一发而动全身”的依赖?
- 技术栈:当前使用的框架(如 React、Vue)、构建工具(Webpack、Vite)、状态管理方案(Redux、Vuex)等,是否存在升级困难(如老项目用 jQuery,难以维护)?
- 团队结构:现有团队是按技术分层(如 UI 组、接口组)还是按业务域划分?团队协作是否存在频繁冲突(如代码合并冲突、发布阻塞)?
- 性能与稳定性:现有 SPA 的首屏加载时间、交互响应速度、崩溃率等指标如何?迁移后需确保这些指标不下降。
二、微前端架构的核心设计要素
迁移的核心是设计一套符合业务的微前端架构,需重点解决“微应用如何拆分、如何协作、如何集成”三大问题。
1. 微应用的拆分策略:高内聚、低耦合
微应用的拆分是迁移的“灵魂”,直接决定后续协作效率和维护成本。拆分需遵循**“业务域边界清晰”**原则,常见拆分方式:
拆分维度 | 适用场景 | 示例(电商场景) |
---|---|---|
按业务域拆分 | 业务模块独立性强,有明确的“职责范围” | 商品模块(列表、详情)、订单模块、支付模块 |
按用户角色拆分 | 不同角色使用的功能差异大(如 C 端用户、B 端商家) | 买家端微应用、商家端微应用 |
按功能层级拆分 | 功能有明显的“上下层”关系(如基础组件、业务组件) | 公共组件微应用、核心业务微应用 |
拆分原则:
- 每个微应用需有独立的业务闭环(如“订单模块”可独立完成下单、支付、退款流程),避免跨应用依赖。
- 尽量减少“跨微应用调用”(如 A 微应用直接修改 B 微应用的 DOM 或状态),若必须调用,需通过标准化接口。
- 拆分粒度不宜过细(避免微应用数量过多,增加管理成本),也不宜过粗(失去微前端的灵活性)。
2. 通信机制:微应用间的“对话规则”
微应用间需通信(如“商品详情页”跳转“订单页”时传递商品 ID),但需避免通信逻辑导致新的耦合。常见方案:
-
发布-订阅模式(EventBus):通过全局事件总线传递消息(如 A 微应用触发
addToCart
事件,购物车微应用监听并处理)。适合简单、低频的通信(如跳转、数据传递)。
优点:低耦合(无需知道对方存在);缺点:事件过多时难以追踪。 -
公共状态服务:将全局共享状态(如用户信息、权限)放在独立的“状态服务”中(如用 Redis 或前端全局 Store),微应用通过 API 读写。适合高频、核心数据共享(如用户登录状态)。
优点:状态统一;缺点:需设计状态更新规则(如防止并发修改冲突)。 -
接口调用:微应用通过暴露“对外 API”供其他应用调用(如订单微应用提供
createOrder(params)
方法)。适合复杂交互(如跨应用提交数据)。
优点:逻辑清晰;缺点:需维护 API 文档,耦合度略高。
原则:微应用内部状态(如表单临时数据)自行管理,仅将“必须共享”的数据放入全局通信层。
3. 路由管理:谁来“指挥”微应用加载?
微前端需一个“主应用(容器应用)”负责路由分发:根据 URL 匹配对应的微应用,并加载/卸载微应用。核心考虑点:
-
路由规则设计:需避免微应用路由冲突(如 A 应用用
/list
,B 应用也用/list
)。解决方案:为每个微应用分配“路由命名空间”(如商品应用路由前缀为/goods
,订单应用为/order
)。 -
路由切换策略:
- 主应用监听路由变化,匹配到目标微应用后,动态加载其资源(JS/CSS)并挂载到 DOM;
- 卸载当前微应用时,需清理其 DOM、事件监听、内存占用(避免内存泄漏)。
-
框架选择:成熟的微前端框架(如 qiankun、single-spa)已内置路由管理能力,可直接复用(如 qiankun 通过
registerMicroApps
注册微应用与路由的映射关系)。
4. 隔离机制:避免“互相干扰”
大型 SPA 的常见问题是“全局污染”(如样式冲突、变量覆盖),微前端需通过隔离机制解决:
-
样式隔离:
- Shadow DOM:将微应用的 DOM 放入 Shadow DOM 中(浏览器原生隔离),但可能影响全局样式(如 UI 组件库的全局主题),且部分浏览器兼容性有限;
-
CSS Modules/BEM 规范:微应用内的样式通过命名隔离(如用
goods__title--active
而非title
); -
Webpack 前缀:通过
css-loader
给微应用样式自动添加前缀(如#goods-app .title
),确保样式仅作用于当前应用。
-
JS 隔离:
-
沙箱机制:主应用为每个微应用创建独立的 JS 执行环境(如 qiankun 的
sandbox
配置),避免全局变量(如window
)被篡改; -
禁止直接操作全局对象:微应用需通过主应用提供的 API 访问全局资源(如
window.localStorage
需通过mainApp.storage.get()
调用)。
-
沙箱机制:主应用为每个微应用创建独立的 JS 执行环境(如 qiankun 的
5. 资源加载:性能与效率的平衡
微应用的资源(JS/CSS)加载直接影响首屏性能,需设计合理的加载策略:
-
加载时机:
- 按需加载:仅当用户访问某路由时,才加载对应微应用的资源(适合非核心模块,如“帮助中心”);
- 预加载:在空闲时间提前加载可能用到的微应用资源(如用户进入商品页后,预加载订单应用资源)。
-
资源共享:避免重复加载公共依赖(如 React、Vue、UI 组件库):
- 用 Webpack Module Federation 共享依赖(主应用或某个微应用作为“宿主”,其他应用复用其依赖);
- 将公共资源放入 CDN,微应用通过
externals
配置引用,减少打包体积。
-
缓存策略:对微应用资源(如 JS 包)设置合理的缓存过期时间(如
Cache-Control: max-age=3600
),配合版本号(如app.js?v=2.1.0
)确保更新生效。
6. 状态管理:全局状态与局部状态的边界
大型 SPA 通常有全局状态(如用户信息、权限)和局部状态(如表单数据),微前端需明确两者的管理边界:
-
全局状态:仅存放“跨微应用共享且稳定”的数据(如用户 ID、登录状态、全局主题),由主应用或独立的“状态服务”管理(如用 Redux Toolkit 或 Pinia 的“全局实例”)。
注意:全局状态需精简,避免成为“状态黑洞”(所有状态都往里塞,导致维护困难)。 -
局部状态:微应用内部的状态(如商品列表的筛选条件、订单表单的输入值)由自身管理(如 React 组件用
useState
,Vue 用reactive
),不依赖外部。
三、迁移实施:增量迁移,平稳过渡
大型 SPA 无法“一刀切”迁移,需采用**“增量迁移”**策略:先搭建基础框架,再逐步替换老模块,同时保留老应用的可用性,直到完全迁移。
1. 迁移步骤(以“主应用+微应用”模式为例)
-
搭建主应用(容器):
主应用负责路由管理、微应用加载、全局通信、样式/JS 隔离等核心能力。初期可基于成熟框架(如 qiankun)快速搭建,无需开发业务功能。 -
选择“试点微应用”:
优先迁移独立、非核心、改动少的模块(如“用户中心”“设置页”),验证架构可行性(如通信、隔离、部署是否符合预期)。避免先迁移核心模块(如“支付流程”),减少风险。 -
老应用与微应用共存:
主应用通过“路由转发”同时支持老 SPA 和新微应用:访问老路由(如/old/goods
)时加载原 SPA 的对应模块;访问新路由(如/new/order
)时加载新微应用。
需开发“适配层”:将老 SPA 的全局变量、事件通过主应用的通信机制暴露给新微应用(如老 SPA 的userInfo
通过EventBus
传递给新应用)。 -
逐步迁移核心模块:
试点验证通过后,按业务优先级迁移核心模块(如商品、订单)。迁移时需先“解耦老代码”(如将老模块的全局依赖改为通过主应用 API 获取),再用新技术栈实现。 -
下线老 SPA:
当所有模块迁移完成后,逐步下线老 SPA 的路由,主应用完全接管所有业务。
2. 团队协作与组织调整
微前端的成功依赖“团队自治”,需同步调整团队结构(康威定律:系统设计反映组织架构):
- 按微应用对应的业务域划分团队(如“商品团队”负责商品微应用的全生命周期),避免按技术分层(如“前端组”“后端组”)。
- 明确团队职责:每个团队独立负责开发、测试、部署、监控,仅需遵守主应用的“接入规范”(如通信 API、路由命名)。
四、风险与应对策略
迁移过程中可能遇到多种风险,需提前预案:
风险类型 | 具体问题 | 应对策略 |
---|---|---|
性能下降 | 首屏加载时间变长(多应用资源加载) | 优化资源加载(预加载、共享依赖)、压缩包体积(Tree-Shaking)、监控性能指标(LCP、FID) |
兼容性问题 | 微应用在低版本浏览器(如 IE)运行异常 | 提前确定兼容范围,用 Babel/PostCSS 转译代码,对不支持的 API(如 Shadow DOM)降级处理 |
调试困难 | 多应用嵌套导致错误定位难(如“哪个应用抛了错”) | 集成统一监控工具(如 Sentry),在错误信息中添加微应用标识;开发环境用sourcemap 定位源码 |
发布冲突 | 微应用独立部署导致版本兼容问题(如 A 应用依赖 B 应用 v1.0,B 应用已升级到 v2.0) | 制定版本兼容规范(如语义化版本),通过灰度发布验证兼容性,主应用支持“回滚到旧版本” |
总结
将大型 SPA 迁移到微前端的核心是“以业务为中心,增量推进,平衡灵活性与复杂度”。需重点考虑:
-
微应用拆分是否符合业务边界;
-
通信、路由、隔离机制是否清晰;
-
迁移过程是否平稳(老应用与新应用共存);
-
团队是否能适应自治协作模式。
一个免费的在线压缩网站超越了付费的压缩软件
大家好,我是 Simon,最近写了一个在线的压缩库,他的效果真的很惊艳,比一些付费的软件表现还好。
起因是我吐槽了,我们网站很多图片使用太大了,每次渲染的很慢,我就说了,你压缩一下再传,他说懒的弄,后来我就分享了一个我的工具,然后他说我这个肯定不好用,因为压缩每一家都有独特的算法,开源的肯定比不上,然后我就结合了 vibe coding 优化了这个网站,支持目录上传、拖拽、等等一系列的方式,能够直接左右对比压缩前后的图片差距,压缩的大小百分比,以及压缩质量的控制,然后再次安利了他,结果吊打了他的付费软件🤣
提供了 B 站的 链接: www.bilibili.com/video/BV1sf…
然后还有在线体验网站: browser-compress-image.netlify.app/
还有开源的 Github 地址:github.com/Simon-He95/…
如果你有一些好的建议,或者一些很棒的开源压缩浏览器端的库推荐,可以集成进来使用
CSS常见问题深度解析与解决方案(第三波)
一、元素居中问题精要
1. 普通div居中
.center-div {
width: 300px;
height: 200px;
/* Flexbox方案 - 现代浏览器首选 */
display: flex;
justify-content: center;
align-items: center;
/* Grid方案 - 最简方式 */
display: grid;
place-items: center;
/* 传统方法 - 固定宽高 */
position: relative;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
2. 浮动元素居中
.float-container::after {
content: "";
display: table;
clear: both;
}
.float-center {
float: left;
position: relative;
left: 50%;
transform: translateX(-50%);
}
3. 绝对定位元素居中
.absolute-center {
position: absolute;
/* 视口居中 */
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
/* 已知尺寸居中 */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 容器内居中 */
inset: 0;
margin: auto;
}
二、动画与视觉细节
4. 动画最小时间间隔
推荐至少16ms(≈60帧/秒)
原因:显示器的刷新率通常为60Hz,每帧间隔16.67ms。超过这个时间会导致动画卡顿,低于这个时间浏览器无法渲染额外帧。
/* 流畅动画的最佳实践 */
.animate {
transition: transform 0.16s ease-out;
animation: pulse 1s infinite;
}
5. position定位详解
absolute与fixed异同:
特性 | absolute | fixed |
---|---|---|
定位基准 | 最近的positioned祖先 | 视口(viewport) |
滚动影响 | 随文档流滚动 | 始终固定在屏幕指定位置 |
共同点 | 脱离文档流 | 脱离文档流 |
应用场景 | 相对父元素的定位元素 | 固定导航、广告、弹窗 |
.example {
/* 共同特性 */
position: absolute | fixed;
top: 0;
left: 0;
z-index: 10;
}
6. CSS颜色表示法
.color-examples {
color: red; /* 关键词 */
color: #FF0000; /* 十六进制 */
color: #F00; /* 简写十六进制 */
color: rgb(255, 0, 0); /* RGB */
color: rgba(255, 0, 0, 0.5); /* RGBA带透明度 */
color: hsl(0, 100%, 50%); /* HSL - 色相/饱和度/亮度 */
color: hsla(0, 100%, 50%, 0.5); /* HSLA带透明度 */
color: oklch(70% 0.2 0); /* 新兴的色彩空间 */
}
7. CSS绘制红色爱心
/* 爱心容器 */
.heart {
position: relative;
width: 100px;
height: 100px;
margin: 50px auto;
}
/* 使用伪元素创建两个半圆 */
.heart::before,
.heart::after {
content: '';
position: absolute;
width: 50px; /* 调整为宽度的一半 */
height: 80px; /* 高度略大于宽度 */
background: #ff6b81; /* 粉色系 */
border-radius: 50px 50px 0 0; /* 只圆化左上和右上 */
}
/* 左半部分 */
.heart::before {
left: 0;
transform: rotate(-45deg); /* 向左旋转45度 */
transform-origin: 100% 100%; /* 以右下角为旋转中心 */
}
/* 右半部分 */
.heart::after {
left: 50%;
transform: rotate(45deg); /* 向右旋转45度 */
transform-origin: 0 100%; /* 以左下角为旋转中心 */
}
8. 百分比高度参考
不是所有情况都基于容器高度:
-
height
:基于容器高度计算 -
padding-top/bottom
:基于容器宽度计算 -
margin-top/bottom
:基于容器宽度计算 -
transform: translateY()
:基于元素自身高度计算
.container {
height: 400px;
width: 600px;
}
.child {
height: 50%; /* 200px (400px的50%) */
padding-top: 10%; /* 60px (600px的10%) */
margin-bottom: 5%; /* 30px (600px的5%) */
transform: translateY(50%); /* 自身高度的50% */
}
三、视觉优化与兼容性
9. 消除transition闪屏
/* 解决方案 */
.element {
transform: translateZ(0); /* 开启GPU加速 */
backface-visibility: hidden; /* 隐藏背面 */
perspective: 1000px; /* 3D透视 */
/* 修复iOS闪屏 */
-webkit-transform-style: preserve-3d;
}
10. 图片格式与应用场景
格式 | 特性 | 适用场景 | WebP支持 |
---|---|---|---|
JPG | 有损压缩、文件较小 | 照片、渐变色图像 | ✓ (需转换) |
PNG | 无损压缩、支持透明通道 | Logo、透明背景图像 | ✓ (需转换) |
GIF | 支持动画、256色限制 | 简单动画、低色彩图像 | ✗ |
WebP | 更优压缩、支持透明/动画 | 现代浏览器中的通用图像格式 | 原生支持 |
<!-- WebP回退方案 -->
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="备用图像">
</picture>
11. box-sizing: border-box详解
* {
box-sizing: border-box; /* 强烈推荐的全局设置 */
}
/* box-sizing对比 */
.content-box {
box-sizing: content-box; /* 默认值: width=内容宽度 */
width: 200px;
padding: 20px; /* 总宽=240px */
}
.border-box {
box-sizing: border-box; /* width=内容+padding+border */
width: 200px;
padding: 20px; /* 总宽保持200px */
}
四、布局系统实践
12. 常用栅格系统对比
系统 | 特点 | 应用场景 | 兼容性 |
---|---|---|---|
Bootstrap | 12列布局、响应式断点 | 企业级应用、快速开发 | 优秀(IE10+) |
CSS Grid | 二维布局、强大对齐控制 | 复杂布局、现代网站 | IE11部分支持 |
Flexbox | 一维布局、弹性项目 | 组件内部布局 | IE10部分支持 |
自定义栅格 | 灵活、按需定制 | 特定设计需求的网站 | 可控 |
13. 响应式设计原理与实践
/* 媒体查询示例 */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
}
/* 移动优先策略 */
.component {
padding: 1rem; /* 基础样式 */
@media (min-width: 992px) {
padding: 2rem; /* 大屏样式 */
}
}
/* IE兼容方案 */
<!--[if lt IE 9]>
<script src="html5shiv.js"></script>
<script src="respond.min.js"></script>
<![endif]-->
14. 选中文本样式定制
::selection {
background-color: #fd79a8; /* 粉红背景 */
color: #fff; /* 白色文字 */
text-shadow: 1px 1px 2px rgba(0,0,0,.2);
}
/* 兼容旧浏览器 */
::-moz-selection {
background: #fd79a8;
color: #fff;
}
五、实用CSS技巧
15. 文本换行处理
/* 强制换行 */
.break-all {
word-break: break-all; /* 任意位置截断 */
overflow-wrap: break-word; /* 保留单词完整 */
}
/* 中文换行 */
.chinese-text {
word-break: break-word;
}
/* 禁止换行 */
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; /* 超出显示省略号 */
}
16. CSS规则结构详解
/* CSS语法结构 */
selector { /* 选择器 */
property: value; /* 属性: 值; */
font-size: 16px; /* 声明 */
}
/* 组合选择器 */
nav a:hover, /* 伪类 */
footer > p:first-child { /* 子元素 */
color: #3498db;
}
/* @规则 */
@import url('reset.css');
@media (min-width: 768px) {...}
@keyframes pulse {...}
17. 自适应高度布局
<div class="container">
<div class="fixed">固定高度 (300px)</div>
<div class="fluid">自适应剩余空间</div>
</div>
.container {
height: 100vh;
display: flex;
flex-direction: column;
}
.fixed {
height: 300px; /* 固定高度 */
background: #74b9ff;
}
.fluid {
flex: 1; /* 填充剩余空间 */
background: #a29bfe;
min-height: 0; /* 防止内容溢出 */
}
18. overflow属性详解
.element {
overflow: visible; /* 默认值:内容溢出容器 */
overflow: hidden; /* 裁剪溢出内容 */
overflow: scroll; /* 始终显示滚动条 */
overflow: auto; /* 仅在需要时显示滚动条 */
/* 单独设置方向 */
overflow-x: hidden;
overflow-y: auto;
/* 滚动条美化 */
scrollbar-width: thin; /* Firefox */
scrollbar-color: #6c5ce7 #dfe6e9;
}
/* Chrome滚动条 */
::-webkit-scrollbar {
width: 8px;
}
六、浏览器兼容与高级技巧
19. IE常见BUG解决方案
/* 双外边距浮动BUG (IE6) */
.floated {
display: inline; /* 触发layout */
}
/* PNG透明度 (IE6) */
.element {
background-image: none;
filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(...);
}
/* min-height (IE6-9) */
.container {
min-height: 500px;
height: auto !important;
height: 500px; /* IE6-7回退 */
}
20. CSS Hack写法参考
/* IE10+ */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {...}
/* Edge */
@supports (-ms-ime-align: auto) {...}
/* IE6-9 */
.element {
color: red\9; /* 所有IE */
*color: blue; /* IE6-7 */
_color: green; /* IE6 */
color: yellow\0/IE9; /* IE9 */
}
/* IE条件注释 */
<!--[if IE 8]>
<link rel="stylesheet" href="ie8.css">
<![endif]-->
21. 字体样式细节
/* Italic与Oblique区别 */
.text {
font-style: italic; /* 使用字体的斜体版本 */
font-style: oblique; /* 常规字体的倾斜版本 */
}
/* 实际应用 */
@font-face {
font-family: 'MyFont';
src: url('myfont-italic.woff') format('woff');
font-style: italic; /* 指定为斜体专用字体 */
}
22. 自适应屏幕高度方案
html, body {
height: 100%; /* 关键设置 */
}
/* 现代方案 */
body {
min-height: 100vh; /* 视口高度 */
min-height: 100dvh; /* 动态视口高度 (移动端优化) */
/* 防止滚动条跳动 */
overflow-y: scroll;
}
/* 原因解释:
1. 百分比高度依赖父元素明确的高度定义
2. 视口单位(vh)直接参考设备屏幕
3. 需要html/body的高度链式传递
*/
23. display属性全集
/* 常用display值 */
.block { display: block; } /* 块级元素 */
.inline { display: inline; } /* 行内元素 */
.inline-block { display: inline-block; } /* 行内块元素 */
.flex { display: flex; } /* 弹性布局 */
.grid { display: grid; } /* 网格布局 */
.none { display: none; } /* 完全隐藏 */
/* 特殊值 */
table { display: table; } /* 表格布局 */
.list-item { display: list-item; } /* 列表项 */
.contents { display: contents; } /* 内容布局 */
24. CSS初始化方案
/* 推荐重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%; /* 移动端调整 */
}
body {
min-height: 100vh;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
img {
max-width: 100%;
height: auto;
display: block;
}
a {
text-decoration: none;
color: inherit;
}
25. BEM规范实践
/* Block-Element-Modifier */
.header { /* 块 */ }
.header__logo { /* 元素 - 属于header */ }
.header__nav { /* 元素 */ }
.header--fixed { /* 修饰符 - 固定状态 */ }
.header__nav-item--active { /* 元素修饰符 */ }
/* 其他常见规范 */
/* OOCSS 分离容器与内容 */
.container {}
.article-title {}
/* SMACSS 分类管理 */
.layout-header {}
.module-alert {}
26. 扇形绘制技巧
/* 方法1:border实现 */
.sector {
width: 0;
height: 0;
border: 100px solid transparent;
border-top: 100px solid #ff6b6b;
border-radius: 50%;
}
/* 方法2:clip-path */
.sector-clip {
width: 200px;
height: 200px;
background: #ff6b6b;
clip-path: polygon(0 0, 100% 0, 50% 50%);
border-radius: 0 100% 0 0;
}
/* 方法3:渐变 */
.sector-gradient {
width: 200px;
height: 200px;
background: conic-gradient(#ff6b6b 0 120deg, transparent 0);
border-radius: 50%;
}
27. 消除inline-block间隙
<!-- 消除间隙的HTML写法 -->
<div class="container">
<span>Item1</span
><span>Item2</span
><span>Item3</span>
</div>
/* CSS解决方案 */
.container {
font-size: 0; /* 父元素字号清零 */
}
.container > * {
display: inline-block;
font-size: 16px; /* 重置子元素字号 */
vertical-align: top; /* 对齐方式统一 */
}
/* 负边距修复 */
.container {
margin-right: -4px;
}
28. 平滑滚动优化
/* 全局平滑滚动 */
html {
scroll-behavior: smooth; /* 基础方案 */
}
/* 容器级平滑滚动 */
.scroll-container {
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch; /* iOS优化 */
/* 性能优化 */
will-change: scroll-position;
overscroll-behavior: contain; /* 避免滚动链 */
}
/* JS实现平滑滚动 */
button.addEventListener('click', () => {
element.scrollTo({
top: 0,
behavior: 'smooth'
});
});
29. !important使用指南
/* 适用场景 */
.override {
color: red !important; /* 1. 覆盖内联样式 */
}
.print-styles {
display: none !important; /* 2. 打印样式强制 */
}
.framework-fix {
width: calc(100% - 20px) !important; /* 3. 覆盖框架样式 */
}
/* 应避免的情况 */
.header {
padding: 10px !important; /* 常规样式中不要使用 */
}
30. 浮动机制与清除
/* 浮动原理 */
.float-box {
float: left; /* 脱离文档流 */
width: 200px;
}
/* 清除浮动方法 */
/* 方法1:clearfix */
.clearfix::after {
content: "";
display: block;
clear: both;
}
/* 方法2:父容器创建BFC */
.container {
overflow: auto;
display: flow-root; /* 推荐方式 */
}
/* 方法3:插入空元素 */
<br style="clear: both">
React 协调器 render 阶段
先来看个全局变量 executionContext
。
executionContext
executionContext
标记当前执行环境,比如当前是 render 阶段或者 commit 阶段。
类型是 number
,取值有 4 种:
type ExecutionContext = number;
export const NoContext = /* */ 0b000;
const BatchedContext = /* 批量/同步更新 */ 0b001;
export const RenderContext = /* render 阶段 */ 0b010;
export const CommitContext = /* commit 阶段 */ 0b100;
初始值为 NoContext
:
// 参考 react/packages/react-reconciler/src/ReactFiberWorkLoop.js
let executionContext: ExecutionContext = NoContext;
两个阶段
和 render 阶段平行的还有个 commit 阶段。
- render 阶段,构建 fiber 树 VDOM
- commit 阶段,将 VDOM 转换成 DOM
两者入口如下:
// 这是每个并发任务的入口点,即通过 Scheduler 的所有内容
function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 没有更新
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}
// 在某些情况下,我们会禁用时间切片:如果 work 过长时间做计算(为了防止饥饿而将其视为“过期”的 work),或者我们处于默认启用同步更新模式。
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes) // 使用时间切片
// 1. render, 构建fiber树VDOM(beginWork|completeWork)
: renderRootSync(root, lanes); // 不用时间切片
// 2. commit, VDOM->DOM
commitRoot(root);
}
renderRootSync
renderRootSync
是 render 阶段入口,包含 4 个步骤:
- render 阶段开始
- workInProgressTransitions 赋值
- 初始化
- 遍历构建 fiber 树
详细代码如下:
function renderRootSync(root: FiberRoot) {
// 1. render阶段开始
const prevExecutionContext = executionContext;
executionContext |= RenderContext; // 注意这里不能用等号,如果使用等号,会覆盖其他 execution,比如如果是批量更新,如果直接覆盖,批量更新就没有了
// 2. workInProgressTransitions 赋值
workInProgressTransitions = getTransitionsForLanes(root, lanes);
// 3. 初始化
prepareFreshStack(root, lanes);
// 4. 遍历构建 fiber 树
workLoopSync();
// 重置 Context 的相关值,入 currentlyRenderingFiber 等
resetContextDependencies();
// 4. render结束,重置 executionContext
executionContext = prevExecutionContext;
workInProgressRoot = null; // 当前正在工作的 root
workInProgressRootRenderLanes = NoLanes;
// 防止有新的更新,再遍历一边更新队列
finishQueueingConcurrentUpdates();
return workInProgressRootExitStatus;
}
function prepareFreshStack(root: FiberRoot): Fiber {
root.finishedWork = null;
workInProgressRoot = root; // FiberRoot
const rootWorkInProgress = createWorkInProgress(root.current, null); // Fiber
if (workInProgress === null) {
workInProgress = rootWorkInProgress; // Fiber
}
// 把 concurrentQueues 的内容添加到 fiber 的 queue 中,即给 fiber 的 lanes、childLanes 赋值
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber) {
const current = unitOfWork.alternate;
// 1. beginWork
// 更新当前 fiber,比如 props/state 更新,生命周期函数执行、Hooks 函数执行等。
// 返回下一个 fiber。
let next = beginWork(current, unitOfWork);
// 把 pendingProps 更新到 memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 1.1 执行自己
// 1.2 (协调,bailout)返回子节点
if (next === null) {
// 没有产生新的work
// 如果不再产生新的 work,那么当前 work 结束
// 2. completeWork,比如生成原生节点,放在 fiber 的 stateNode 属性上
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
// 当前 Fiber 为 null
ReactCurrentOwner.current = null
}
// 深度优先遍历,子节点、兄弟节点、叔叔节点、爷爷的兄弟节点...(王朝的故事)
function completeUnitOfWork(unitOfWork: Fiber) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next = completeWork(current, completedWork);
if (next !== null) {
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber as Fiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
sibling-index:我用这个画时钟表盘
如果你需要画一个时钟表盘,你会怎么做?用12个div元素,使用CSS的transform
属性来旋转它们,来将他们放在正确的位置。但是,你会发现,你需要使用JavaScript来计算每个div元素的旋转角度, 当前你也可以提前计算好,但终究是要计算的。一但钟表半径变化,你就需要重新计算。
那如何我们现在添加一些限制条件呢
<div class="panel">
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
</div>
使用上面的html片段,不使用nth-*
之类的选择器,不使用+
选择器。纯CSS应该如何实现一个表盘呢
先添加一些基础样式
.panel {
width: 200px;
height: 200px;
border-radius: 50%;
border: 3px solid #000;
position: relative;
}
.digit {
position: absolute;
left: 90px;
top: 90px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
}
填充12个刻度
再没有JS的情况下,我们可以使用::after
伪元素来实现向页面添加显示内容。那1-12这些数字应该实现如何呢。在html中有什么情况会自动为元素填充序号呢?没错,有序列表。那我们这里其实也类似,在CSS中有counter
功能
.panel {
counter-reset: digit;
}
.digit {
counter-increment: digit;
&::after {
content: counter(digit);
}
}
这样每个div中都会显示一个数字。接下来,我们需要将这些数字旋转到正确的位置。我们可以使用transform
属性来实现。问题是,我们如何知道每个数字的旋转角度呢?我们可以使用sibling-index
函数来实现。这个函数可以返回一个元素在兄弟元素当中的索引。我们可以使用这个函数来计算每个数字的旋转角度。
.digit {
--angle: calc((sibling-index() - 3) * 30deg);
transform: translate(calc(cos(var(--angle)) * 90px), calc(sin(var(--angle)) * 90px));
}
附上完整代码
<style>
.panel {
width: 200px;
height: 200px;
border-radius: 50%;
border: 3px solid #000;
position: relative;
counter-reset: digit;
}
.digit {
position: absolute;
left: 90px;
top: 90px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
counter-increment: digit;
--angle: calc((sibling-index() - 3) * 30deg);
transform: translate(calc(cos(var(--angle)) * 90px), calc(sin(var(--angle)) * 90px));
&::after {
content: counter(digit);
}
}
</style>
<div class="panel">
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
<div class="digit"></div>
</div>
效果如下
兼容性
目前只有chrome最新版本支持这个函数,其他浏览器还没有支持。谨慎使用
JavaScript :字符串模板——优雅编程的基石
在 JavaScript 开发中,字符串拼接曾是开发者最频繁的操作之一。
随着 ES6 的普及,字符串模板(Template String)作为一项革命性特性,彻底改变了我们处理字符串的方式,它不仅简化了代码逻辑,而且提升了可读性和可维护性。
本文将结合数组方法、Symbol 类型等内容,深入解析字符串模板的核心原理和实际应用。
一、字符串模板:优雅的字符串操作
1. 基础语法与核心优势
字符串模板通过反引号(`
)定义,支持多行字符串和嵌入变量,语法如下:
const name = "狗·德川家康·薛定谔·保留";
const age = 3;
const sentence = `我的猫 ${name} 今年 ${age * 5} 岁.`;
console.log(sentence);
// 输出:我的猫 狗·德川家康·薛定谔·保留 今年 15 岁.
核心优势:
-
多行字符串支持:无需
\n
或字符串拼接即可实现多行文本。 -
变量嵌入:通过
${expression}
语法直接嵌入表达式,告别繁琐的+
拼接。 - 代码可读性:逻辑清晰,减少冗余字符(如引号闭合、转义符号)。
对比传统方式:
// 传统拼接
const sentenceOld = "我的猫 " + name + " 今年 " + (age * 5) + " 岁.";
// 字符串模板
const sentenceNew = `我的猫 ${name} 今年 ${age * 5} 岁.`;
2. 多行字符串的实际应用
字符串模板对多行字符串的处理尤其强大,尤其适合生成 HTML 结构或格式化文本:
const html = `
<div class="user-card">
<h2>${user.name}</h2>
<p>年龄:${user.age}岁</p>
<p>地址:${user.address}</p>
</div>
`;
与 DOM 操作结合:
const friends = [
{ name: '刘备', age: 520, address: '桃园' },
{ name: '关羽', age: 114, address: '同上' },
{ name: '张飞', age: 514, address: '俺也一样' }
];
const ul = document.getElementById('friends');
ul.innerHTML = friends.map(friend => `
<li>
${friend.name} , <i>${friend.age}岁</i> , <b>${friend.address}</b>
</li>
`).join('');
关键点解析:
-
map
方法:遍历数组并返回新数组,每个元素为<li>
的 HTML 字符串。 -
.join('')
:将数组转换为字符串,避免innerHTML
中出现逗号分隔符。 -
字符串模板优势:无需手动拼接
<li>
标签,结构清晰,格式自然保留。
二、数组方法与字符串模板的协同:map
的深度解析
1. map
方法的核心特性
map
是 ES6 为数组提供的高阶函数,用于将数组中的每个元素映射为新形式,返回新数组:
const numbers = [1, 2, 3];
const squares = numbers.map(n => n * n);
console.log(squares); // [1, 4, 9]
与字符串模板结合的典型场景:
- 数据格式转换:将 JSON 数据转化为 HTML 元素。
- 动态渲染:根据数据生成动态内容(如表格、列表等)。
代码案例:
const users = [
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'User' }
];
const tableBody = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.role}</td>
</tr>
`).join('');
document.getElementById('user-table').innerHTML = tableBody;
2. map
与 forEach
的对比
特性 | map |
forEach |
---|---|---|
返回值 | 新数组(映射后的结果) | undefined |
适用场景 | 数据转换、生成新数组 | 仅需遍历,无需返回新数组 |
示例 | numbers.map(n => n * 2) |
arr.forEach(item => console.log(item)) |
三、Symbol 类型:对象属性的唯一性保障
1. Symbol 的基本概念
Symbol
是 ES6 引入的原始数据类型,表示独一无二的值,常用于定义对象的私有属性:
const sym1 = Symbol();
const sym2 = Symbol('desc'); // 可选描述信息
console.log(sym1 === sym2); // false
作为对象键的使用:
const ID = Symbol('id');
const user = {
name: 'Alice',
[ID]: 123,
[Symbol('age')]: 20
};
console.log(user[ID]); // 123
console.log(user[Symbol('age')]); // undefined(Symbol 是唯一的)
关键优势:
- 避免键名冲突:Symbol 作为键时,不会与字符串键冲突。
-
私有属性模拟:Symbol 键无法通过
for...in
遍历,适合模拟私有属性。
代码案例:
const user = {
name: 'Alice',
[Symbol('id')]: 123,
age: 18
};
for (let key in user) {
console.log(key); // 输出 name 和 age,不会包含 Symbol 键
}
2. Symbol 与字符串键的区别
特性 | 字符串键(String Key) | Symbol 键(Symbol Key) |
---|---|---|
唯一性 | 可能冲突(如 age vs 'age' ) |
始终唯一 |
遍历性 | 可遍历(for...in ) |
默认不可遍历(需 Object.getOwnPropertySymbols() ) |
适用场景 | 公共属性 | 私有属性、元数据 |
四、枚举类型:用 Symbol 实现状态管理
1. 枚举的实现原理
JavaScript 本身没有原生的枚举类型,但可以通过对象或 Symbol 模拟:
const status = {
ready: Symbol('ready'),
running: Symbol('running'),
done: Symbol('done')
};
let state = status.ready;
if (state === status.ready) {
console.log('准备就绪');
}
优势:
- 常量不可变:Symbol 值不会被意外修改。
- 避免命名冲突:Symbol 是唯一的,不会与其他键冲突。
五、实际应用场景:从数据到 UI 的完整流程
1. 动态渲染用户列表
结合字符串模板、map
和 DOM 操作,实现动态渲染:
<ul id="user-list"></ul>
<script>
const friends = [
{ name: '刘备', age: 520, address: '桃园' },
{ name: '关羽', age: 114, address: '同上' },
{ name: '张飞', age: 514, address: '俺也一样' }
];
const userList = document.getElementById('user-list');
userList.innerHTML = users.map(user => `
<li>
<strong>${user.name}</strong>(<em>${user.age}岁</em>)<br>
<small>地址:${user.address}</small>
</li>
`).join('');
</script>
2. 数据格式转换:JSON 到 HTML 表格
将 JSON 数据转换为 HTML 表格,展示 map
与字符串模板的协同作用:
const products = [
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 }
];
const table = `
<table border="1">
<thead>
<tr><th>ID</th><th>名称</th><th>价格</th></tr>
</thead>
<tbody>
${products.map(p => `
<tr>
<td>${p.id}</td>
<td>${p.name}</td>
<td>¥${p.price}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.body.innerHTML = table;
六、总结
1. 核心知识点回顾
-
字符串模板:通过反引号和
${}
实现多行字符串和变量嵌入。 -
map
方法:用于数据转换,与字符串模板结合可高效生成动态内容。 - Symbol 类型:提供唯一键,适合定义私有属性和避免命名冲突。
- 枚举模拟:通过 Symbol 实现状态管理,提升代码健壮性。
2. 实战建议
- 优先使用字符串模板:替代传统的字符串拼接,提升代码可读性。
-
合理使用
map
:在需要返回新数组的场景中,优先选择map
而非forEach
。 - Symbol 用于敏感属性:对需要保护的属性(如私有 ID),使用 Symbol 键。
- 避免过度嵌套:字符串模板嵌套层级过深时,可拆分为多个变量或函数。
3. 常见误区与解决方案
-
问题 1:
map
返回的数组未用.join('')
转换,导致innerHTML
中出现逗号。-
解决方案:始终用
.join('')
处理数组。
-
解决方案:始终用
-
问题 2:Symbol 键被意外暴露。
-
解决方案:通过
Object.getOwnPropertySymbols()
访问 Symbol 键。
-
解决方案:通过
50行代码搞定OneCode摄像头插件:快速定制实战指南
引言
在工业监控、人脸识别等场景中,摄像头集成是常见需求。本文将以OneCode平台的xui.UI.Camera
组件为例,展示如何用50行核心代码实现一个功能完备的摄像头插件,涵盖设备访问、视频流显示和拍照功能,并提炼OneCode插件开发的核心要素。
一、核心代码实现(50行精简版)
xui.Class("xui.UI.Camera", "xui.UI", {
Instance: {
// 初始化摄像头
initCamera: function() {
const video = this.getSubNode("H5").dom;
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => video.srcObject = stream)
.catch(e => xui.log("摄像头访问失败:", e));
},
// 拍照功能
captureImage: function() {
const canvas = document.createElement("canvas");
const video = this.getSubNode("H5").dom;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0);
return canvas.toDataURL("image/png");
}
},
Static: {
Templates: {
tagName: 'div',
H5: { tagName: 'video', autoplay: true, playsinline: true },
COVER: { tagName: 'div', style: "background-image:url("+xui.ini.img_bg+");" }
},
DataModel: {
width: { $spaceunit:1, ini:'34em' },
height: { $spaceunit:1, ini:'25em' },
captureBtn: { ini: true, action: function(v) { v && this.boxing().initCamera(); } }
},
RenderTrigger: function() { this.boxing().initCamera(); }
}
});
二、技术点深度解析
2.1 媒体设备访问机制
核心依赖Web API getUserMedia
实现摄像头数据流捕获:
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => video.srcObject = stream)
- 权限处理:自动触发浏览器摄像头权限请求
- 流处理:直接将MediaStream对象赋值给video元素的srcObject
- 错误处理:捕获设备访问失败场景(无摄像头/权限拒绝)
2.2 OneCode组件三要素
- 模板系统(Templates)
Templates: {
tagName: 'div', // 根容器
H5: { tagName: 'video', ... }, // 视频播放元素
COVER: { ... } // 覆盖层
}
采用声明式DOM结构,支持动态属性绑定(如autoplay: {_autoplay}
)
- 数据模型(DataModel)
DataModel: {
width: { $spaceunit:1, ini:'34em' }, // 支持响应式单位
captureBtn: { // 功能开关属性
ini: true,
action: function(v) { ... } // 属性变化回调
}
}
-
$spaceunit
: 启用单位转换(em/px自动适配) -
action
: 属性值变化时的触发逻辑
- 渲染触发器(RenderTrigger)
RenderTrigger: function() {
this.boxing().initCamera(); // 组件渲染完成后初始化摄像头
}
组件生命周期关键节点,确保DOM就绪后执行初始化逻辑
2.3 拍照功能实现
利用Canvas API实现视频帧捕获:
captureImage: function() {
const canvas = document.createElement("canvas");
// 匹配视频分辨率
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 绘制当前视频帧
canvas.getContext("2d").drawImage(video, 0, 0);
return canvas.toDataURL("image/png"); // 返回base64图片
}
- 分辨率适配:使用video实际分辨率确保图像清晰
- 数据格式:支持base64编码,便于后续上传或显示
三、OneCode插件开发核心要素
3.1 类继承体系
xui.Class("xui.UI.Camera", "xui.UI", { ... })
- 继承
xui.UI
基类获取组件基础能力 - 复杂场景可继承特定组件(如
xui.UI.Audio
)
3.2 实例方法设计
-
初始化方法:
initCamera()
- 处理设备访问 -
功能方法:
captureImage()
- 实现核心业务逻辑 -
事件处理:可扩展
onCaptureSuccess
等回调方法
3.3 属性驱动开发
通过DataModel定义可配置属性,实现组件灵活性:
// 支持动态调整分辨率
resolution: {
ini: "720p",
listbox: ["480p", "720p", "1080p"],
action: function(v) {
const constraints = { video: { width: { ideal: v=="1080p"?1920:v=="720p"?1280:640 } } };
// 重新初始化摄像头
}
}
3.4 跨浏览器兼容性
- 前缀处理:针对旧浏览器提供兼容性封装
const getUserMedia = navigator.mediaDevices.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
- 特性检测:提前检查浏览器支持情况
if (!getUserMedia) {
this.getSubNode("H5").html("您的浏览器不支持摄像头功能");
}
四、快速扩展指南
4.1 添加拍照按钮
// 在Templates中添加按钮
CAPTURE_BTN: { tagName: 'button', text: '拍照', onclick: 'capture' }
// 添加点击事件处理
Instance: { capture: function() { const img = this.captureImage(); ... } }
4.2 实现图片上传
uploadImage: function(base64Data) {
xui.ajax({ url: '/upload', method: 'POST', data: { image: base64Data } });
}
4.3 视频录制功能
基于MediaRecorder API扩展:
startRecord: function() {
this.recorder = new MediaRecorder(this.stream);
this.chunks = [];
this.recorder.ondataavailable = e => this.chunks.push(e.data);
this.recorder.start();
}
五、最佳实践
- 权限管理:提供明确的权限申请提示
- 错误处理:覆盖设备未找到、权限拒绝等场景
- 资源释放:组件销毁时停止视频流
destroy: function() {
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
}
- 性能优化:根据网络状况动态调整分辨率
结语
通过本文示例,我们展示了如何用50行核心代码实现一个基础摄像头插件。OneCode平台的组件化设计极大简化了开发流程,开发者只需关注核心业务逻辑。实际项目中,可基于此扩展人脸识别、二维码扫描等高级功能,满足不同场景需求。
提示:完整代码可参考
xui.UI.Camera
组件源码,更多API细节请查阅OneCode官方文档。
智能前端小魔术,让图片开口说单词
这几天做了个小项目 —— 用户上传图片后,AI能自动识别出对应的英文单词,还能生成例句和语音。整个过程踩了不少坑,也有很多值得说道的细节,今天就从头到尾捋一捋实现思路,尤其是那些容易被忽略的技术点。
从主组件开始
任何 React 项目都需要一个主组件来串联各个功能,App.jsx
就是这个项目的 “大脑”,负责管理全局状态和协调各个模块。先看最基础的结构:
import { useState } from "react";
import "./App.css";
import PictureCard from "./components/PictureCard";
import { generateAudio } from "./lib/audio";
function App() {
// 后面会逐步添加状态和方法
return (
<div className='container'>
{/* 内容后续填充 */}
</div>
);
}
export default App;
这个组件的核心任务有三个:存储项目中所有需要共享的数据(比如识别出的单词、语音地址)、处理 AI 接口调用逻辑、把数据和方法传递给子组件。
定义状态
首先要明确,这个项目需要存储哪些数据?我列了一下,至少包括这些:
function App() {
// 英文单词
const [word, setWord] = useState("请上传图片");
// 英文例句
const [sentence, setSentence] = useState("");
// 语音地址
const [audio, setAudio] = useState("");
// 控制详情区域是否展开
const [detailExpand, setDetailExpand] = useState(false);
// 图片的预览地址
const [imgPreview, setImgPreview] = useState("");
// 单词的详细解释
const [explanation, setExplanation] = useState([]);
// 解释中问句的回复内容
const [expReply, setExpReply] = useState([]);
}
这些状态各自有明确的用途:word
展示核心单词,sentence
展示例句,audio
存储语音地址…… 最关键的是,这些状态会在不同组件间流转,比如 word
和 audio
会传给子组件 PictureCard
展示,而 imgPreview
则会在详情区域显示。
搭建页面结构
状态定义好了,接下来要考虑如何把这些数据展示给用户。页面结构其实很简单,主要分为两部分:图片上传区和结果展示区。
return (
<div className='container'>
<PictureCard
word={word}
audio={audio}
uploadImg={uploadImg}
/>
{/* 结果展示区域 */}
<div className="output">
<div className="sentence">{sentence}</div>
{/* 详情展开/收起区域 */}
<div className="details">
<button onClick={() => setDetailExpand(!detailExpand)}>Talk about it</button>
{
detailExpand ? (
<div className="expand">
<img src={imgPreview} alt="preview"/>
{
explanation.map((explanation, index) => (
<div key={index} className="explanation">
{explanation}
</div>
))
}
{
expReply.map((reply, index) => (
<div key={index} className="reply">
{reply}
</div>
))
}
</div>
): (
<div className="fold" />
)
}
</div>
</div>
</div>
);
这段代码的逻辑很清晰:
- 用
PictureCard
组件处理图片上传,把需要展示的数据(word
、audio
)和处理方法(uploadImg
)传进去 - 结果展示区分为两部分:直接显示的例句,和可展开的详情(包含图片、解释、回复)
- 点击按钮时通过
setDetailExpand
切换详情的显示状态,这是 React 中常见的条件渲染方式
图片上传与预览
PictureCard
组件是用户交互的入口,负责接收用户上传的图片并实时预览,同时把图片数据传给 App
组件处理。先看它的基础结构:
import { useState } from "react";
import "./style.css";
const PictureCard = (props) => {
const { word, audio, uploadImg } = props;
const [imgPreview, setImgPreview] = useState(
"https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png"
);
return (
<div className="card">
{/* 上传相关内容 */}
</div>
);
};
export default PictureCard;
这个组件的核心任务是:让用户能选择图片 → 读取图片内容 → 实时预览 → 把图片数据传给父组件。
实现图片选择与预览功能
return (
<div className="card">
<input
type="file"
id="selectImage"
accept=".jpg,.jpeg,.png,.gif"
onChange={uploadImgData}
/>
{/* label 与 input 关联,点击图片就会打开文件选择框 */}
<label htmlFor="selectImage" className="upload">
<img src={imgPreview} alt="preview" />
</label>
</div>
);
选择图片需要一个文件输入框,但原生的输入框样式不好控制,所以用了个小技巧:把 input[type="file"]
隐藏起来,用 label
关联它,这样就能自定义上传按钮的样式。点击 label
里的图片时,其实触发的是文件选择框,体验会好很多。
接下来是关键的 uploadImgData
方法,它负责读取用户选择的图片并转换格式:
const uploadImgData = async (e) => {
// 获取用户选择的第一个文件
const file = e.target.files?.[0];
if (!file) return;
// 用 Promise 包装,方便父组件知道图片处理完成的时机
return new Promise((resolve, reject) => {
// 创建 FileReader 实例,用于读取文件内容
const reader = new FileReader();
// 以 DataURL 格式读取文件(会把文件转成 base64 字符串)
reader.readAsDataURL(file);
reader.onload = () => {
const data = reader.result;
setImgPreview(data);
uploadImg(data);
resolve(data);
};
// 读取失败的回调
reader.onerror = (error) => {
reject(error);
};
});
};
最关键的是 uploadImgData
函数,这里用到了 FileReader
API。刚开始我想直接把文件对象传给后端,后来发现前端预览必须先读文件内容。readAsDataURL
方法会把文件转换成 base64 格式的字符串,长得像 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
这种。
为什么要用 base64?因为它可以直接作为 img
标签的 src
属性值,不用上传到服务器就能预览。当 reader.onload
触发时,reader.result
就是转换好的 base64 字符串,把它存到 imgPreview
状态里,图片就实时显示出来了。
这里踩过一个坑:如果取消选择文件,e.target.files
会是空的,所以必须加个判断 if (!file) return
,否则会报错。另外,用 Promise 包装读取过程,是为了让父组件能知道图片什么时候处理完,方便后续调用 API。
调用月之暗面 API
图片数据传到 App
组件后,下一步就是调用 AI 接口分析图片内容。这部分逻辑在 uploadImg
方法里,这个方法是 PictureCard
组件传图片数据时触发的。
准备提示词
调用 AI 接口时,提示词(prompt)非常关键,直接决定返回结果的质量。我专门写了一段提示词,明确告诉 AI 需要做什么、返回什么格式的数据:
// 定义提示词,指导 AI 如何分析图片
const picPrompt = `
请分析图片内容,找出最能描述图片的一个英文单词,尽量选择 A1-A2 难度的基础词汇。
必须严格按照以下 JSON 格式返回结果,不要添加任何额外内容:
{
"image_description": "对图片内容的简要描述",
"representative_word": "最能代表图片的英文单词",
"example_sentence": "包含该单词的简单英文例句",
"explanation": "用英文解释这个单词,要求每句单独一行,以 Look at... 开头,最后加一个与日常生活相关的问句",
"explanation_reply": ["对问句的第一个回复(英文)", "对问句的第二个回复(英文)"]
}
`;
提示词里强调了几点:单词难度(A1-A2)、严格的 JSON 格式、解释的结构(每句一行 + 问句)。刚开始没限制格式时,AI 经常返回大段文本,解析起来很麻烦,加了格式限制后就顺畅多了。
实现 API 调用逻辑
有了提示词,就可以编写调用接口的代码了:
const uploadImg = async (imageData) => {
setImgPreview(imageData);
setWord("分析中...");
// 月之暗面 API 的地址
const endpoint = "https://api.moonshot.cn/v1/chat/completions";
// 请求头
const headers = {
"Content-Type": "application/json",
// 从环境变量获取 API 密钥,避免明文暴露
Authorization: `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
};
try {
// 发送 POST 请求
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
model: "moonshot-v1-8k-vision-preview",
messages: [
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: imageData }
},
{
type: "text",
text: picPrompt
}
]
}
],
stream: false
})
});
// 解析接口返回的 JSON 数据
const data = await response.json();
const replyData = JSON.parse(data.choices[0].message.content);
// 更新状态,让页面显示识别结果
setWord(replyData.representative_word);
setSentence(replyData.example_sentence);
setExplanation(replyData.explanation.split("\n"));
setExpReply(replyData.explanation_reply);
// 调用 TTS 接口生成语音(后面详细讲)
const audioUrl = await generateAudio(replyData.example_sentence);
setAudio(audioUrl);
} catch (error) {
// 出错时显示错误提示
setWord("分析失败,请重试");
console.error("API 调用出错:", error);
}
};
这段代码的关键步骤:
-
imageData
是从子组件传过来的 base64 字符串,直接作为图片 URL 传给 API,省了先上传到服务器的步骤。 - 请求头里的
Authorization
用了环境变量VITE_KIMI_API_KEY
,这是为了安全,密钥不能明文写在代码里,用 Vite 的环境变量管理很方便。 - 调用 API 时指定了
model: "moonshot-v1-8k-vision-preview"
,这是月之暗面支持图片识别的模型。 - 拿到返回结果后,先用
JSON.parse
解析,因为 AI 返回的是字符串格式的 JSON,必须转成对象才能用。 - 最后调用
generateAudio
生成语音,把例句读出来,这个函数的实现是另一个重点。
按理说我们拿到了数据应该去渲染页面,但是我为了偷懒直接把页面放在前面了😁😁
展示单词和语音播放按钮
除了上传功能,这个组件还要显示识别出的单词和语音播放按钮。在 return 里补充这部分内容:
return (
<div className="card">
<input type="file" id="selectImage" ... />
<label htmlFor="selectImage" className="upload">
<img src={imgPreview} alt="preview" />
</label>
<div className="word">{word}</div>
{/* 语音播放按钮,只有当 audio 有值时才显示 */}
{audio && (
<div className="playAudio" onClick={playAudio}>
<img
width="20px"
src="https://res.bearbobo.com/resource/upload/Omq2HFs8/playA-3iob5qyckpa.png"
alt="play"
/>
</div>
)}
</div>
);
再实现播放语音的方法:
// 播放语音的方法
const playAudio = () => {
const audioEle = new Audio(audio);
audioEle.play();
};
这里的逻辑很简单:当父组件传来 audio
地址时,渲染播放按钮;点击按钮时,用 Audio
构造函数创建音频实例并播放。
实现 TTS 功能
最后一步是把生成的例句转成语音,这部分逻辑放在 lib/audio.js
里,涉及到 base64 解码和 Blob 处理,是项目的技术亮点之一。
调用 TTS 接口生成音频数据
首先实现调用 TTS 服务的方法,获取音频的 base64 数据:
// lib/audio.js
export const generateAudio = async (text) => {
// 从环境变量获取配置
const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
const appId = import.meta.env.VITE_AUDIO_APP_ID;
const clusterId = import.meta.env.VITE_AUDIO_CLUSTER_ID;
const voiceName = import.meta.env.VITE_AUDIO_VOICE_NAME;
// TTS 接口地址
const endpoint = "/tts/api/v1/tts";
// 请求头
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer;${token}`
};
// 构造请求体,不同服务的参数格式可能不同
const payload = {
app: {
appid: appId,
token,
cluster: clusterId
},
user: {
uid: "ai-word-user" // 自定义用户 ID
},
audio: {
voice_type: voiceName, // 发音人
encoding: "mp3", // 音频格式
rate: 24000, // 采样率
speed_ratio: 1.0, // 语速
emotion: "neutral" // 情感
},
request: {
reqid: Math.random().toString(36).substring(2, 10), // 随机请求 ID
text, // 要转换的文本(例句)
text_type: "plain" // 文本类型为纯文本
}
};
try {
// 调用 TTS 接口
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify(payload)
});
const data = await response.json();
// 把返回的 base64 音频数据转成可播放的 URL
return getAudioUrl(data.data);
} catch (error) {
console.error("语音生成失败:", error);
return null;
}
};
这个函数的作用是把例句文本传给 TTS 服务,拿到音频数据。这里的 reqid
用随机字符串生成,是为了避免请求冲突。返回的 data.data
是 base64 编码的音频内容,不能直接用,调用 getAudioUrl
方法把数据转成浏览器能播放的格式。
将 base64 音频数据转换为可播放的 URL
TTS 服务返回的是 base64 编码的音频数据,不能直接播放,需要将其转换为浏览器可以识别的格式:
const getAudioUrl = (base64Data) => {
// 1. 解码 base64 字符串,得到二进制字符串
const byteCharacters = atob(base64Data);
// 2. 创建字节数组
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
// 3. 创建 Uint8Array(8 位无符号整数数组)
const byteArray = new Uint8Array(byteNumbers);
// 4. 创建 Blob 对象(二进制大对象)
const blob = new Blob([byteArray], { type: 'audio/mp3' });
// 5. 生成可播放的 URL
return URL.createObjectURL(blob);
};
这个过程涉及几个关键步骤:
-
解码 base64:使用
atob()
函数将 base64 字符串解码为原始二进制数据 -
转换为字节数组:将二进制字符串转换为数字数组,每个数字对应一个字节
-
创建类型化数组:使用
Uint8Array
将普通数组转换为 JavaScript 可以处理的二进制数据 -
创建 Blob 对象:将二进制数据封装为 Blob 对象,并指定 MIME 类型为
audio/mp3
-
生成 URL:使用
URL.createObjectURL()
为 Blob 对象生成一个临时 URL,这个 URL 可以直接作为<audio>
元素的 src
为什么不直接使用 base64 字符串作为音频源?虽然可以这样做,但性能会较差,尤其是对于较大的音频文件。而使用 Blob URL 可以让浏览器更高效地处理音频数据,并且在不再需要时可以通过 URL.revokeObjectURL()
释放资源。
好了,整个项目的流程就是这样了,下面让我们上传一张图片来看看具体效果吧!
这个项目虽然功能不复杂,但把图片处理、AI 接口调用、语音生成这些知识点串起来了。尤其是前端直接处理文件和二进制数据的部分,以前总觉得很深奥,实际做起来才发现,掌握 FileReader
、Blob
、URL
这些 API 后,很多需求都能迎刃而解。最后想问一句,这个苹果是不是看起来很好吃?
vite 插件
1. vite 插件介绍
-
服务器启动阶段:
options
和buildStart
钩子会在服务启动时被调用。 -
请求响应阶段: 当浏览器发起请求时,Vite 内部依次调用
resolveId
、load
和transform
钩子。 -
服务器关闭阶段: Vite 会依次执行
buildEnd
和closeBundle
钩子。 -
moduleParsed: 模块解析后触发,允许你修改模块的元数据。 在开发阶段不被调用,因为 Vite 使用的是热重载和即时模块更新,不需要完整的模块解析阶段。
-
renderChunk:生成输出块时调用,允许你自定义生成的代码块。 开发阶段不调用,因为 Vite 主要依赖浏览器的即时更新,而非生成最终的打包输出。
1.1 强制插件排序
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...typescript2(),
apply: 'build',
},
{
...image(),
enforce: 'pre',
},
],
})
- pre: 在vite核心插件之前调用这个插件
- post: 在vite核心插件之后调用这个插件
- 默认:在 Vite 核心插件之后调用该插件
1.2 情景应用
- 开发 (serve) 和生产 (build) 默认都会调用
- apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:
注意:名字 vite-plugin-xxx
2. 虚拟模块
- 虚拟模块就像是你凭空创造出来的 JS 文件,不在电脑上真实存在,但可以被其他模块像普通文件一样导入使用。
适用场景 - 动态生成配置 运行时变量注入 按需生成工具函数
2.1
import type { Plugin } from 'vite'
// 定义虚拟模块ID
const virtualModuleId = 'virtual:fibonacci'
const resolvedVirtualModuleId = '\0' + virtualModuleId
export default function virtualFibPlugin(): Plugin {
return {
name: 'vite-plugin-virtual-fib',
// 解析虚拟模块ID
resolveId(id) {
console.log(id, 'resolveId')
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
// 加载虚拟模块内容
load(id) {
console.log(id, 'load')
if (id === resolvedVirtualModuleId) {
return `
// 斐波那契数列实现
export function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}
// 记忆化版本
export function memoFib(n, memo = {}) {
if (n in memo) return memo[n]
if (n <= 1) return n
memo[n] = memoFib(n - 1, memo) + memoFib(n - 2, memo)
return memo[n]
}
`
}
}
}
}
注意1:加 \0 公共插件使用 virtual:插件名 格式(如 virtual:posts)
注意2: 虚拟模块在生产的时候 最好显示配置一下
export default {
plugins: [virtualPlugin()],
build: {
rollupOptions: {
plugins: [virtualPlugin()] // 显式注册
}
}
}
🤔:不配置啥后果 虚拟模块在生产环境中的可用性取决于具体场景
-
基本虚拟模块(仅生成静态内容):
- 通常可以工作:如果插件只是生成简单的静态内容
- 原因:Vite 会继承主插件数组中的插件配置
-
复杂虚拟模块(依赖特定钩子或转换):
- ❌ 可能失败:如果插件依赖 build 阶段的特定钩子
- 报错表现:Cannot find module 'virtual:xxx' 或生成的内容不正确
3. vite特有钩子
- config 在解析 Vite 配置前调用。钩子接收原始用户配置
- configResolved 在解析 Vite 配置后调用 使用这个钩子读取和存储最终解析的配置
- configureServer 是用于配置开发服务器的钩子
- configurePreviewServer 用于定制预览服务器的钩子,类似configureServer 但专门用vite preview 命令启动的预览服务器。
- transformIndexHtml 专门用来修改 index.html 文件的钩子
- handleHotUpdate 自定义热模块替换(HMR)行为的钩子
3.1 配置处理钩子
config - 修改配置
config(config, { command, mode }) {
// command: 'serve'开发模式 | 'build'生产模式
// mode: 'development' | 'production' | 自定义模式
if (command === 'serve') {
return {
server: {
port: 3000, // 修改开发服务器端口
open: true // 自动打开浏览器
}
}
}
// 生产环境配置
return {
build: {
minify: 'terser'
}
}
}
configResolved - 配置确认
const myPlugin = () => {
let viteConfig // 用于存储配置
return {
name: 'my-plugin',
// configResolved 钩子
configResolved(resolvedConfig) {
// 存储最终解析的配置
viteConfig = resolvedConfig
console.log('当前运行模式:', viteConfig.command)
},
// 在其他钩子中使用配置
transform(code, id) {
if (viteConfig.command === 'serve') {
console.log('开发模式处理:', id)
} else {
console.log('生产构建处理:', id)
}
return code
}
}
}
configResolved(config) {
this.isDev = config.command === 'serve'
this.isProduction = !this.isDev
},
load(id) {
if (id === virtualModuleId) {
return this.isDev
? `export const mode = 'development'`
: `export const mode = 'production'`
}
}
3.2 开发阶段钩子
-
configureServer 开发服务器的钩子
- 添加新的功能比如中间件 或者文件监听
configureServer(server: ViteDevServer): void | (() => void)
参数 server 包含以下重要属性:
- middlewares: Connect 中间件实例
- httpServer: 底层 HTTP 服务器
- watcher: 文件监听器
- ws: WebSocket 服务器
- transformRequest(): 用于转换模块内容
eg
import type { Plugin } from 'vite'
import type { ViteDevServer } from 'vite'
export default function vitePluginTest(): Plugin {
let devServer: ViteDevServer
return {
name: 'vite-plugin-test',
configureServer(server) {
devServer = server
// 监听src目录下的所有文件变化(修正了watcher.add的用法)
server.watcher.add('src/**/*')
// 添加API接口
server.middlewares.use('/api/data', (_, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(
JSON.stringify({
data: '测试数据',
timestamp: Date.now()
})
)
})
// 请求日志中间件
server.middlewares.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] 请求: ${req.method} ${req.url}`)
next()
})
// 监听文件变化(修正了监听逻辑)
server.watcher.on('change', (file) => {
console.log(`文件发生变化: ${file}`)
if (file.startsWith('src/')) {
console.log('src目录文件变化,刷新页面')
server.ws.send({
type: 'full-reload',
path: '*'
})
}
})
},
handleHotUpdate({ file }) {
if (file.endsWith('.tsx')) {
console.log('TSX文件修改,触发全量刷新')
devServer.ws.send({
type: 'full-reload',
path: '*'
})
}
}
}
}
注意
configureServer(server) {
// 这个会在Vite中间件之前执行
server.middlewares.use(...)
// 返回的函数会在Vite中间件之后执行
return () => {
server.middlewares.use(...)
}
}
- transformIndexHtml 可以定制html内容
可以 自动插入标签 修改内容
transformIndexHtml(html, ctx) {
const tags: Array<{
tag: string
injectTo: 'head' | 'body' | 'head-prepend' | 'body-prepend'
children?: string
attrs?: Record<string, string>
}> = []
const isDev = !!ctx.server
if (isDev) {
tags.push({
tag: 'script',
injectTo: 'body-prepend',
children: 'console.log("开发模式已启动")'
})
} else {
tags.push({
tag: 'meta',
injectTo: 'head',
attrs: { name: 'robots', content: 'index,follow' }
})
}
const newHtml = html.replace(
'<title>Vite + React + TS</title>',
'<title>我的定制应用</title>'
)
// 返回符合 Vite 要求的类型
return {
html: newHtml,
tags
}
},
- handleHotUpdate模块热更新
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.tsx')) {
console.log('TSX文件修改,触发全量刷新')
devServer.ws.send({
type: 'full-reload',
path: '*'
})
}
// 2. 只处理项目文件,忽略node_modules
return ctx.modules.filter((module) => !module?.id?.includes('node_modules'))
}
graph TD
A[采购食材 config] --> B[确认菜单 configResolved]
B --> C[员工培训 configureServer]
C --> D[开门营业 buildStart]
D --> E{客人点餐}
E -->|HTML订单| F[摆盘 transformIndexHtml]
E -->|食材请求| G[接单→备货→烹饪 resolveId→load→transform]
E -->|变更需求| H[厨房监控 handleHotUpdate]
H --> I{是否打烊}
I -->|是| J[清理 buildEnd → 关店 closeBundle]
4. 例子
4.1 自动引入antd组件
一个自动引入插件 可以自动为你的项目按需导入 API,无需手动编写 import 语句,自动导入,按需加载。
- resolvers 是 unplugin-auto-import 中的一个高级配置选项,用于自定义解析自动导入的组件或工具函数的方式。
思考: 问题
- 我们如果开发的时候 使用antd组件 可以使用这个插件 导入很多antd组件,让vite.config文件内容很多 不太美观
- 并且antd组件中没有默认的字母开头 比如 Button 可能会造成与其他自定义组件冲突,想一下是不是可以给antd 组件加个前缀 就像AButton
所以的插件功能主要实现 自动导入全部antd组件 并且可以自定义前缀
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import AutoImport from 'unplugin-auto-import/vite'
import antdResolver from './unplugin-auto-import-antd'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
AutoImport({
imports: [
'react',
{
// antd: [
// 'Button',
// 'Input'
// ]
}
],
resolvers: [
// antdResolver({
// prefix: 'A', // 可选:为所有组件添加 A 前缀
// packageName: 'antd' // 可选:默认为 'antd'
// }),
{
type: 'component',
resolve: (name: string) => {
console.log('resolve', name)
const supportedComponents = ['AButton', 'Button', 'AInput', 'Table'] // 扩展这个列表
if (supportedComponents.includes(name)) {
return {
from: 'antd',
name: 'Input',
as: `${name}` // 统一添加A前缀
}
}
return undefined
}
}
],
dts: true, // 生成类型声明文件
eslintrc: {
enabled: true // 生成 eslint 配置
}
})
]
})
4.2 svg直接作为组件导入
import type { Plugin } from 'vite'
import fs from 'node:fs/promises'
import { transform } from '@svgr/core'
import { transform as esbuildTransform } from 'esbuild'
interface SvgrOptions {
defaultExport?: 'url' | 'component' // 导出类型:URL字符串 或 React组件
svgrOptions?: Record<string, any> // 自定义SVGR配置
}
export default function svgrPlugin(options: SvgrOptions = {}): Plugin {
// 设置默认值:默认导出为组件,空SVGR配置
const { defaultExport = 'component', svgrOptions = {} } = options
return {
name: 'vite-plugin-svgr',
// transform 钩子:转换文件内容
async transform(_, id) {
// 只处理 .svg 文件
if (!id.endsWith('.svg')) return
try {
// 1. 读取 SVG 文件内容
const svg = await fs.readFile(id, 'utf-8')
// 2. 使用 SVGR 将 SVG 转换为 React 组件代码
const componentCode = await transform(
svg, // SVG 原始内容
{
...svgrOptions, // 用户自定义配置
// 核心插件配置开始
plugins: [
'@svgr/plugin-jsx', // 转换 SVG 为 JSX
'@svgr/plugin-prettier' // 格式化生成的代码
],
// 其他重要配置
typescript: true, // 生成 TS 兼容代码
jsxRuntime: 'automatic', // 使用新版 JSX 运行时
exportType: 'named', // 使用命名导出
// 自定义模板:控制组件输出结构
template: ({ componentName, jsx }, { tpl }) => {
return tpl`
const ${componentName} = (props) => ${jsx};
export { ${componentName} };
`
}
},
{ componentName: 'ReactComponent' } // 设置组件名称
)
// 3. 清理生成的代码
let jsCode = componentCode
.replace(/^\/\*.*?\*\/\s*/gms, '') // 移除注释
.replace(/\n+/g, '\n') // 压缩空行
.trim()
// 4. 处理导出逻辑
if (defaultExport === 'url') {
// URL 模式:默认导出 SVG 路径
jsCode = `
${jsCode}
export default ${JSON.stringify(id)};
`.trim()
} else {
// 组件模式:默认导出 React 组件
jsCode = `
${jsCode}
export default ReactComponent;
`.trim()
}
// 5. 使用 esbuild 转换 JSX 为浏览器可执行代码
const result = await esbuildTransform(jsCode, {
loader: 'jsx', // 指定为 JSX 类型
jsx: 'automatic', // 使用新版 JSX 转换
sourcefile: id, // 源文件路径(用于 sourcemap)
format: 'esm', // 输出 ESM 格式
target: 'es2020', // 目标 ES 版本
logLevel: 'silent' // 不输出日志
})
// 6. 返回转换后的代码
return {
code: result.code,
map: result.map || null
}
} catch (error) {
// 错误处理:回退到原始 SVG 路径导出
console.error(`SVG转换失败 [${id}]:`, error)
return {
code: `export default ${JSON.stringify(id)};`,
map: null
}
}
}
}
}
Web开发中的文件上传
在Web开发中,文件上传是一个常见的功能需求。无论是用户头像的上传、文档的提交还是图片的分享,文件上传都扮演着重要的角色。本文将详细介绍文件上传的消息格式和实现方式,帮助你更好地理解和实现文件上传功能。
一、文件上传的消息格式
文件上传的本质仍然是一个数据提交过程,只是数据量通常较大。在实践中,人们逐渐形成了一种共识,文件上传默认使用multipart/form-data
格式。这种格式允许在一个请求中发送多个部分(part),每个部分可以包含不同的数据类型,如文本字段和文件数据。
请求格式示例
以下是一个典型的文件上传请求格式:
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="小仙女.jpg"
Content-Type: image/jpeg
(文件二进制数据)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
admin
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="password"
123123
----WebKitFormBoundary7MA4YWxkTrZu0gW
关键点说明
- 请求方法:文件上传通常使用POST请求。
-
Content-Type:
multipart/form-data
,浏览器会自动分配一个定界符boundary
。 -
请求体格式:请求体被
boundary
分割成多个部分,每个部分是一个键值对。对于文件数据部分,还会包含文件的本地名称和MIME类型。
二、文件上传的实现
在现代Web开发中,文件上传通常通过表单或JavaScript实现。以下是一个基于表单的文件上传实现示例:
HTML表单实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传示例</title>
</head>
<body>
<form action="http://localhost:8000/api/upload" method="post" enctype="multipart/form-data">
<label for="avatar">选择文件:</label>
<input type="file" id="avatar" name="avatar">
<br>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<br>
<label for="password">密码:</label>
<input type="password" id="password" name="password">
<br>
<button type="submit">上传文件</button>
</form>
</body>
</html>
JavaScript实现
使用JavaScript可以实现更灵活的文件上传功能,例如在上传前进行文件大小和类型的验证。以下是一个使用FormData
和fetch
API实现文件上传的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传示例</title>
</head>
<body>
<input type="file" id="avatar">
<input type="text" id="username">
<input type="password" id="password">
<button id="uploadButton">上传文件</button>
<script>
document.getElementById('uploadButton').addEventListener('click', function() {
const avatar = document.getElementById('avatar').files[0];
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const formData = new FormData();
formData.append('avatar', avatar);
formData.append('username', username);
formData.append('password', password);
fetch('http://localhost:8000/api/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('上传成功:', data);
})
.catch(error => {
console.error('上传失败:', error);
});
});
</script>
</body>
</html>