阅读视图
2025传感器大会在郑州启幕
Tauri(十九)——实现 macOS 划词监控的完整实践
背景
为了提高 Coco AI 的用户使用率,以及提供快捷操作等,我给我们 Coco AI 也增加了划词功能。
接下来就介绍一下如何在 Tauri v2 中实现“划词”功能(选中文本的实时检测与前端弹窗联动),覆盖 macOS 无障碍权限、坐标转换、多屏支持、前端事件桥接与性能/稳定性策略。
功能概述
- 在系统前台 App 中选中文本后,后端读取选区文本与鼠标坐标,通过事件主动推给前端。
- 前端根据事件展示/隐藏弹窗(或“快查”面板),并在主窗口中同步输入/状态。
- 提供“全局开关”,随时启停划词监控。
关键点与设计思路
- 权限:macOS 读取选区依赖系统“无障碍(Accessibility)”权限;首次运行时请求用户授权。
- 稳定性:对选区读取做轻量重试与去抖,避免弹窗闪烁。
-
坐标:Quartz 坐标系为“左下角为原点”,前端常用“左上角为原点”;需要对
y做翻转。 - 多屏:在多显示器场景下,根据鼠标所在显示器与全局边界计算统一坐标。
- 交互保护:当 Coco 自己在前台时,暂不读取选区,避免把弹窗交互误判为空选区。
-
事件协议:统一向前端发两个事件:
-
selection-detected:选区文本与坐标(或空字符串表示隐藏) -
selection-enabled:开关状态
-
后端实现(Tauri v2 / Rust)
- 定义事件载荷与全局开关,导出命令给前端调用。
- 在启动入口中开启监控线程,不断读取选区并发事件。
/// 事件载荷:选中文本与坐标(逻辑点、左上为原点)
#[derive(serde::Serialize, Clone)]
struct SelectionEventPayload {
text: String,
x: i32,
y: i32,
}
use std::sync::atomic::{AtomicBool, Ordering};
/// 全局开关:默认开启
static SELECTION_ENABLED: AtomicBool = AtomicBool::new(true);
#[derive(serde::Serialize, Clone)]
struct SelectionEnabledPayload {
enabled: bool,
}
/// 读写开关并广播
pub fn is_selection_enabled() -> bool { SELECTION_ENABLED.load(Ordering::Relaxed) }
fn set_selection_enabled_internal(app_handle: &tauri::AppHandle, enabled: bool) {
SELECTION_ENABLED.store(enabled, Ordering::Relaxed);
let _ = app_handle.emit("selection-enabled", SelectionEnabledPayload { enabled });
}
/// Tauri 命令:供前端调用开关
#[tauri::command]
pub fn set_selection_enabled(app_handle: tauri::AppHandle, enabled: bool) {
set_selection_enabled_internal(&app_handle, enabled);
}
#[tauri::command]
pub fn get_selection_enabled() -> bool { is_selection_enabled() }
- 启动监控线程:权限校验、选区读取、坐标转换与事件发送。
#[cfg(target_os = "macos")]
pub fn start_selection_monitor(app_handle: tauri::AppHandle) {
use std::time::Duration;
use tauri::Emitter;
// 同步初始开关状态到前端
set_selection_enabled_internal(&app_handle, is_selection_enabled());
// 申请/校验无障碍权限(macOS)
{
let trusted_before = macos_accessibility_client::accessibility::application_is_trusted();
if !trusted_before {
let _ = macos_accessibility_client::accessibility::application_is_trusted_with_prompt();
}
let trusted_after = macos_accessibility_client::accessibility::application_is_trusted();
if !trusted_after {
return; // 未授权则不启动监控
}
}
// 监控线程
std::thread::spawn(move || {
use objc2_core_graphics::CGEvent;
use objc2_core_graphics::{CGDisplayBounds, CGGetActiveDisplayList, CGMainDisplayID};
#[cfg(target_os = "macos")]
use objc2_app_kit::NSWorkspace;
// 计算鼠标全局坐标(左上原点),并做 y 翻转
let current_mouse_point_global = || -> (i32, i32) {
unsafe {
let event = CGEvent::new(None);
let pt = objc2_core_graphics::CGEvent::location(event.as_deref());
// 多屏取全局边界并翻转 y
// ...(详见源码的显示器遍历与边界计算)
// 返回 (x_top_left, y_flipped)
// ... existing code ...
(/*x*/0, /*y*/0)
}
};
// Coco 在前台时不读选区,避免交互中误判空
let is_frontmost_app_me = || -> bool {
#[cfg(target_os = "macos")]
unsafe {
let workspace = NSWorkspace::sharedWorkspace();
if let Some(frontmost) = workspace.frontmostApplication() {
let pid = frontmost.processIdentifier();
let my_pid = std::process::id() as i32;
return pid == my_pid;
}
}
false
};
// 状态机与去抖
let mut popup_visible = false;
let mut last_text = String::new();
let stable_threshold = 2; // 连续一致≥2次视为稳定
let empty_threshold = 2; // 连续空≥2次才隐藏
let mut stable_text = String::new();
let mut stable_count = 0;
let mut empty_count = 0;
loop {
std::thread::sleep(Duration::from_millis(30));
if !is_selection_enabled() {
if popup_visible {
let _ = app_handle.emit("selection-detected", "");
popup_visible = false;
last_text.clear();
stable_text.clear();
}
continue;
}
let front_is_me = is_frontmost_app_me();
let selected_text = if front_is_me {
None // 交互期间不读选区
} else {
read_selected_text_with_retries(2, 35) // 轻量重试
};
match selected_text {
Some(text) if !text.is_empty() => {
// 稳定性检测
// ... existing code ...
if stable_count >= stable_threshold {
if !popup_visible || text != last_text {
let (x, y) = current_mouse_point_global();
let payload = SelectionEventPayload { text: text.clone(), x, y };
let _ = app_handle.emit("selection-detected", payload);
last_text = text;
popup_visible = true;
}
}
}
_ => {
// 非前台且空选区:累计空次数后隐藏
// ... existing code ...
}
}
}
});
}
- 读取选区(AXUIElement):优先系统级焦点,其次前台 App 的焦点/窗口;仅读取
AXSelectedText。
#[cfg(target_os = "macos")]
fn read_selected_text() -> Option<String> {
use objc2_application_services::{AXError, AXUIElement};
use objc2_core_foundation::{CFRetained, CFString, CFType};
// 优先系统级焦点 AXFocusedUIElement,失败则回退到前台 App/窗口焦点
// 跳过当前进程(Coco)避免误判
// 成功后读取 AXSelectedText,转为 String 返回
// ... existing code ...
Some(/*selected text*/ String::new())
}
#[cfg(target_os = "macos")]
fn read_selected_text_with_retries(retries: u32, delay_ms: u64) -> Option<String> {
// 最多重试 N 次:缓解 AX 焦点短暂不稳定
// ... existing code ...
None
}
前端事件桥接
-
事件名称
-
selection-enabled:载荷{ enabled: boolean },用于同步开关状态 -
selection-detected:载荷{ text: string, x: number, y: number }或""(隐藏)
-
-
监听与联动建议
- 通过
platformAdapter.listenEvent("selection-detected", ...)已完成桥接。 - 收到带文本的事件后,渲染弹窗;收到
""时隐藏。 - 在主窗口中同步搜索/聊天输入与模式。例如配合
useSearchStore/useAppStore更新searchValue、isChatMode、askAiMessage等。
- 通过
// 伪示例:监听 selection-detected 并联动 UI
function useListenSelection() {
// ... existing code ...
platformAdapter.listenEvent("selection-detected", (payload) => {
if (payload === "") {
// 隐藏弹窗
// ... existing code ...
return;
}
const { text, x, y } = payload as { text: string; x: number; y: number };
// 展示弹窗(使用 x, y 定位)
// 同步到主窗口输入或 AI 询问
// ... existing code ...
});
}
Tauri v2 集成与命令注册
- 在后端入口(如
main.rs):- 注册命令:
set_selection_enabled、get_selection_enabled - 应用启动后调用一次
start_selection_monitor(app_handle.clone())开启监控线程
- 注册命令:
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
set_selection_enabled,
get_selection_enabled
])
.setup(|app| {
let handle = app.handle().clone();
#[cfg(target_os = "macos")]
{
start_selection_monitor(handle);
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running coco app");
}
权限与配置
- macOS 无障碍(Accessibility)权限
- 首次启动会触发系统授权提示;用户需在“系统设置 → 隐私与安全 → 辅助功能”中允许 Coco。
- 代码中使用
macos_accessibility_client检查与提示,不需额外Info.plist键。
- Tauri v2 Capabilities
- Tauri 对前端 API 能力有更细粒度的限制;如需事件、命令调用等,确保
tauri.conf.json的capabilities配置允许相应操作。
- Tauri 对前端 API 能力有更细粒度的限制;如需事件、命令调用等,确保
稳定性与性能策略
- 去抖与重试
-
stable_threshold = 2:相同文本稳定两次再触发事件,减少闪烁与误报 -
empty_threshold = 2:空选区累计两次再隐藏,避免短暂抖动导致过度隐藏
-
- 轮询间隔
-
30ms足够流畅,实际可根据功耗与体验权衡调整
-
- 交互保护
- 前台为 Coco 时不读选区,避免把弹窗交互过程误读为空选区,从而误触隐藏
坐标与多屏支持
- Quartz 坐标系为“左下为原点”,很多前端布局为“左上为原点”
- 通过计算全局高度并翻转
y,确保前端定位直观
- 通过计算全局高度并翻转
- 多屏场景
- 遍历所有活动显示器,计算全局最左、最上、最下边界,统一映射全局坐标
- 根据鼠标实际所在显示器确定相对坐标,兼顾跨屏切换的平滑性
常见问题与排查
- 未授权导致“没有任何事件”
- 检查“系统设置 → 隐私与安全 → 辅助功能”是否勾选 Coco
- 前端没有响应
selection-detected- 确认事件监听正确(命名与载荷形态)、确保主窗口同步更新输入与模式
- 坐标不正确或弹窗偏移
- 排查坐标系转换(y 翻转)、多屏边界计算是否符合实际布局
- 弹窗闪烁或频繁隐藏
- 调整
stable_threshold/empty_threshold与轮询间隔;也可对文本变化设更严格的稳定条件
- 调整
测试清单
- 授权流程:首次运行提示、授权后是否正常读取
- 多屏场景:跨屏移动鼠标后坐标是否正确、弹窗位置是否稳定
- 交互过程:点击弹窗与主窗口时是否停止读取选区、不会误判空而隐藏
- 文本变化:快速划词切换时是否平滑、不会频繁闪烁
小结
- 划词功能的核心在于 “权限 → 获取选区 → 稳定性处理 → 事件联动 → 前端渲染” 这条链路。
- Tauri v2 在能力管理与事件桥接上更清晰,结合 macOS 的 AX 接口与坐标转换,可以构建稳定、体验良好的系统级“快查”能力。
开源共建,欢迎 Star ✨:github.com/infinilabs/…
上海乐高乐园累计接待游客量破百万人次,未来将启动二期扩建
IDG资本捐赠1000万港元,驰援香港大埔火灾救援
AI相关需求推升行业景气度,PCB上游高端基材货缺价涨
西部陆海新通道班列已累计发送货物突破500万标箱
Vue3 如何实现图片懒加载?其实一个 Intersection Observer 就搞定了
大家好,在当今图片密集的网络环境中,优化图片加载已成为前端开发的重要任务。今天我们分享一下怎么使用 Vue3 实现图片的懒加载功能。
什么是图片懒加载?
假如你打开一个有大量图片的页面,如果所有图片同时加载,会导致页面卡顿、流量浪费,特别是对于那些需要滚动才能看到的图片。
懒加载技术就是解决这个问题的方案,只有当图片进入或即将进入可视区域的时候,才加载它们。
效果预览:
完整示例代码可在文末获取
实现原理
我们的Vue3懒加载实现基于以下核心技术:
1. Intersection Observer API
这是现代浏览器提供的强大API,可以高效监听元素是否进入可视区域,而无需频繁计算元素位置,性能远优于传统的滚动监听方式。
// 创建观察器
observer.value = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) { // 元素进入可视区域
loadImage();
observer.value.unobserve(entry.target); // 加载后停止观察
}
});
}, {
rootMargin: '50px 0px', // 提前50px开始加载
threshold: 0.1 // 元素10%可见时触发
});
2. 组件化设计
我们将懒加载功能封装为独立的 LazyImage 组件,提高代码复用性和可维护性。
代码实现详解
组件模板结构
<div class="lazy-image-container" ref="container">
<img
v-if="isLoaded && !hasError"
:src="actualSrc"
:alt="alt"
class="lazy-image"
:style="{ opacity: imageOpacity }"
@load="onLoad"
@error="onError"
/>
<div v-else-if="hasError" class="image-placeholder">
<div class="error-message">图片加载失败</div>
<button @click="retryLoad" style="margin-top: 10px;">重试</button>
</div>
<div v-else class="image-placeholder">
<div class="spinner"></div>
<div>加载中...</div>
</div>
</div>
组件包含三种状态:
- 加载中:显示旋转加载动画
- 加载完成:显示实际图片,带有淡入效果
- 加载失败:显示错误信息和重试按钮
核心逻辑实现
状态管理
setup(props, { emit }) {
const isLoaded = ref(false); // 是否已加载
const hasError = ref(false); // 是否加载失败
const imageOpacity = ref(0); // 图片透明度(用于淡入效果)
const observer = ref(null); // Intersection Observer实例
const container = ref(null); // 容器DOM引用
const actualSrc = ref(''); // 实际图片地址
// ...
}
使用Vue3的Composition API,我们可以更清晰地组织代码逻辑。
图片加载控制
const loadImage = () => {
if (props.slowLoad) {
// 模拟慢速网络 - 延迟2秒加载
setTimeout(() => {
actualSrc.value = props.src;
isLoaded.value = true;
}, 2000);
} else {
// 正常加载
actualSrc.value = props.src;
isLoaded.value = true;
}
};
这个函数根据slowLoad属性决定是否模拟慢速网络,便于测试不同网络条件下的表现。
生命周期管理
onMounted(() => {
// 创建并启动Intersection Observer
observer.value = new IntersectionObserver((entries) => {
// 观察逻辑...
});
if (container.value) {
observer.value.observe(container.value);
}
});
onUnmounted(() => {
// 组件卸载时清理观察器
if (observer.value) {
observer.value.disconnect();
}
});
确保在组件销毁时正确清理资源,避免内存泄漏。
错误处理与重试机制
const onError = () => {
hasError.value = true;
emit('error'); // 向父组件发送错误事件
};
const retryLoad = () => {
hasError.value = false;
isLoaded.value = false;
// 重新触发观察
if (observer.value && container.value) {
observer.value.observe(container.value);
}
};
良好的错误处理机制可以提升用户体验,让用户在图片加载失败时有机会重试。
应用该组件
在主组件中使用懒加载
<div class="gallery">
<div
v-for="(image, index) in images"
:key="index"
class="image-card"
>
<lazy-image
:src="image.url"
:alt="image.title"
:slow-load="networkSlow"
@loaded="onImageLoaded"
@error="onImageError"
></lazy-image>
<div class="image-info">
<div class="image-title">{{ image.title }}</div>
<div class="image-description">{{ image.description }}</div>
</div>
</div>
</div>
功能控制与统计
我们的主组件提供了实用的控制功能:
- 添加更多图片:动态加载更多图片
- 重置图片:恢复初始状态
- 模拟网络速度:切换正常/慢速网络模式
- 加载统计:实时显示已加载和失败的图片数量
进一步优化
在实际项目中,还可以考虑以下优化:
- 图片压缩与格式选择:使用WebP等现代格式,减小文件体积
- 渐进式加载:先加载低质量预览图,再加载高清图
- 预加载关键图片:对首屏内的关键图片不使用懒加载
- 使用CDN加速:通过内容分发网络提高图片加载速度
Github示例代码:github.com/1344160559-…
总结
Vue3图片懒加载是一个简单但极其实用的优化技术。通过Intersection Observer API和Vue3的响应式系统,我们可以以少量代码实现高效的懒加载功能,显著提升页面性能和用户体验。
这个实现不仅适用于图片展示类网站,也可以应用于任何需要优化资源加载的Vue3项目。希望本文能帮助你理解和实现这一重要前端优化技术!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+MySQL+Vue实现文件共享系统》
HarmonyOS 应用开发基础案例(三):使用DevEco Studio高效开发(篇一)
HUAWEI DevEco Studio是基于IntelliJ IDEA Community开源版本打造,为运行在HarmonyOS系统上的应用和元服务提供一站式的开发平台。
作为一款开发工具,除了具有基本的代码开发、编译构建及调测等功能外,DevEco Studio还具有如下特点:
- 高效智能代码编辑:支持ArkTS、JS、C/C++等语言的代码高亮、代码智能补齐、代码错误检查、代码自动跳转、代码格式化、代码查找等功能,提升代码编写效率。
- 多端双向实时预览:支持UI界面代码的双向预览、实时预览、动态预览、组件预览以及多端设备预览,便于快速查看代码运行效果。
- 多端设备模拟仿真:提供HarmonyOS本地模拟器,支持Phone等设备的模拟仿真,便捷获取调试环境。
- DevEco Profiler性能调优:提供实时监控能力和场景化调优模板,便于全方位的设备资源监测,采集数据覆盖多个维度,为开发者带来高效、直通代码行的调优体验。
1. 工程管理
DevEco Studio的工程管理涵盖应用和元服务的结构,其中APP Pack由HAP包和pack.info文件组成;支持工程目录的两种视图形式,并展示基于ArkTS Stage模型的目录结构;同时提供多种工程模板及创建新工程的详细流程,帮助开发者高效搭建与管理项目。
1.1. 工程介绍
1.1.1. App包结构
在进行应用/元服务开发前,我们应该掌握应用/元服务的逻辑结构。
应用/元服务发布形态为APP Pack(Application Package),它是由一个或多个HAP(Harmony Ability Package)包以及描述APP Pack属性的pack.info文件组成。
一个HAP在工程目录中对应一个Module,它是由代码、资源、三方库及应用/元服务配置文件组成,HAP可以分为Entry和Feature两种类型。
- Entry: 应用的主模块,作为应用的入口,提供了应用的基础功能。
- Feature: 应用的动态特性模块,作为应用能力的扩展,可以根据用户的需求和设备类型进行选择性安装。
Stage模型应用程序包结构如图1所示。
图1 App包结构
1.1.2. 切换工程视图
DevEco Studio工程目录结构提供工程视图和Ohos视图。工程视图(Project)展示工程中实际的文件结构,Ohos视图会隐藏一些编码中不常用到的文件,并将常用到的文件进行重组展示,方便开发者查询或定位所需编辑的模块或文件。
工程创建或打开后,默认显示工程视图,如果要切换到Ohos视图,在左上角单击Project > Ohos进行切换 。 如图2所示。
图2 工程视图目录截图
1.2. 工程目录结构
ArkTS Stage模型支持API Version 10及以上版本,其工程目录结构如图3所示:
图3 某工程目录结构
- AppScope > app.json5:应用的全局配置信息。
- entry: 应用/元服务模块,编译构建生成一个HAP。
-
- src > main > ets:用于存放ArkTS源码。
- src > main > ets > entryability:应用/元服务的入口。
- src > main > ets > pages:应用/元服务包含的页面。
- src > main > resources: 用于存放应用/元服务模块所用到的资源文件,如图形、多媒体、字符串、布局文件等。如表4-1所示。
| 资源目录 | 资源文件说明 |
|---|---|
| base>element | 包括字符串、整型数、颜色、样式等资源的json文件。每个资源均由json格式进行定义,例如:- - - boolean.json:布尔型 |
- color.json:颜色
- float.json:浮点型
- intarray.json:整型数组
- integer.json:整型
- pattern.json:样式
- plural.json:复数形式
- strarray.json:字符串数组
- string.json:字符串值 |
| base>media | 多媒体文件,如图形、视频、音频等文件,支持的文件格式包括: .png、 .gif、 .mp3、 .mp4等。 | | rawfile | 用于存储任意格式的原始资源文件。rawfile不会根据设备的状态去匹配不同的资源,需要指定文件路径和文件名进行引用。 |
表4-1 资源目录与文件说明
-
- src > main > module.json5:Stage模型模块配置文件,主要包含HAP的配置信息、应用在具体设备上的配置信息以及应用的全局配置信息。
- build-profile.json5: 当前的模块信息、编译信息配置项,包括buildOption、targets配置等。
- hvigorfile.ts:模块级编译构建任务脚本。
- oh-package.json5:描述三方包的包名、版本、入口文件(类型声明文件)和依赖项等信息。
- oh_modules:用于存放三方库依赖信息,包含应用/元服务所依赖的第三方库文件。
- build-profile.json5: 应用级配置信息,包括签名、产品配置等。
- hvigorfile.ts: 应用级编译构建任务脚本。
- oh-package.json5: 描述全局配置,如:依赖覆盖(overrides)、依赖关系重写(overrideDependencyMap)和参数化配置(parameterFile)等。
1.3. 工程模板介绍
DevEco Studio支持多种品类的应用/元服务开发,预置丰富的工程模板,可以根据工程向导轻松创建适应于各类设备的工程,并自动生成对应的代码和资源模板。同时,DevEco Studio还提供了多种编程语言供开发者进行应用/元服务开发,包括ArkTS、JS和C/C++。如图4所示。
图4 创建项目
工程模板支持的开发语言及模板说明如表4-2所示:
| 模板名称 | 说明 |
|---|---|
| Empty Ability | 用于Phone、Tablet、2in1、Car设备的模板,展示基础的Hello World功能。 |
| Native C++ | 用于Phone、Tablet、2in1、Car设备的模板,作为应用调用C++代码的示例工程,界面显示“Hello World”。 |
| [CloudDev]Empty Ability | 端云一体化开发通用模板。 |
| [Lite]Empty Ability | 用于Lite Wearable设备的模板,展示了基础的Hello World功能。可基于此模板,修改设备类型及RuntimeOS,进行小型嵌入式设备开发。 |
| Flexible Layout Ability | 用于创建跨设备应用开发的三层工程结构模板。三层工程结构包含common(公共能力层)、features(基础特性层)、products(产品定制层)。 |
| Embeddable Ability | 用于开发支持被其他应用嵌入式运行的元服务的工程模板。 |
表4-2 模版说明
1.4. ****创建一个新的工程
当您开始开发一个应用/元服务时,首先需要根据工程创建向导,创建一个新的工程,工具会自动生成对应的代码和资源模板。
- 通过如下两种方式,打开工程创建向导界面。
- 如果当前未打开任何工程,可以在DevEco Studio的欢迎页,选择Create Project开始创建一个新工程。
- 如果已经打开了工程,可以在菜单栏选择File > New > Create Project来创建一个新工程。
- 根据工程创建向导,选择创建Application或Atomic Service。再选择需要的Ability工程模板,然后单击Next。
- 在工程配置页面,需要根据向导配置工程的基本信息。
- Project name:工程的名称,可以自定义,由大小写字母、数字和下划线组成。
- Bundle name:标识应用的包名,用于标识应用的唯一性。
- Save location:工程文件本地存储路径,由大小写字母、数字和下划线等组成,不能包含中文字符。
- Compatible SDK:兼容的最低API Version。
- Module name: 模块的名称。
- Device type: 该工程模板支持的设备类型。
如图5所示。
图5 配置项目
- 单击Finish,工具会自动生成示例代码和相关资源,等待工程创建完成。
2. 代码编辑
本节介绍代码阅读、生成/补全、实时检查与修复、重构等常用操作。DevEco Studio支持多语言代码的高亮显示、快捷跳转和智能格式化,提供自动补全、构造器快速生成等功能,能够实时检测并修复代码错误,同时支持提取方法、重命名等重构操作,有效提升编码效率。
2.1. 代码阅读
DevEco Studio支持使用多种语言进行应用/元服务的开发,包括ArkTS、JS和C/C++。在编写应用/元服务阶段,可以通过掌握代码编写的各种常用技巧,来提升编码效率。
2.1.1. 代码高亮
支持对代码关键字、运算符、字符串、类、标识符、注释等进行高亮显示,您可以打开File > Settings(macOS为DevEco Studio > Preferences)面板,在Editor > Color Scheme自定义各字段的高亮显示颜色 。 默认情况下,您可以在Language Defaults中设置源代码中的各种高亮显示方案,该设置将对所有语言生效;如果您需要针对具体语言的源码高亮显示方案进行定制,可以在左侧边栏选择对应的语言,然后取消“Inherit values from”选项后设置对应的颜色即可。如图6所示。
图6 代码高亮设置
2.1.2. 代码跳转
在编辑器中,可以按住Ctrl键(macOS为Command键),鼠标单击代码中引用的类、方法、参数、变量等名称,自动跳转到定义处。若单击定义处的类、变量等名称,当仅有一处引用时,可直接跳转到引用位置;若有多处引用,在弹窗中可以选择想要查看的引用位置。
2.1.3. 代码格式化
代码格式化功能可以帮助您快速的调整和规范代码格式,提升代码的美观度和可读性。默认情况下,DevEco Studio已预置了代码格式化的规范,您也可以个性化的设置各个文件的格式化规范,设置方式如下:在File > Settings > Editor > Code Style(macOS为DevEco Studio > Preferences > ****Editor ****> ****Code Style)下,选择需要定制的文件类型,如ArkTS,然后自定义格式化规范即可。
图7 代码格式化设置
在使用代码格式化功能时,您可以使用快捷键Ctrl + Alt + L(macOS为Option+Command +L) 可以快速对选定范围的代码进行格式化。
如果在进行格式化时,对于部分代码片段不需要进行自动的格式化处理,可以通过如下方式进行设置:
- 在File > Settings >Editor > Code Style(macOS为DevEco Studio > Preferences > Editor > Code Style),单击“Formatter”,勾选“Turn formatter on/off with markers in code comments”。如图8所示。
图8 代码样式
在不需要进行格式化操作的代码块前增加“//@formatter:off”,并在该代码块的最后增加“//@formatter:on”,即表示对该范围的代码块不需要进行格式化操作。如图9所示。
图9 代码格式化
2.1.4. 代码折叠
支持对代码块的快速折叠和展开,既可以单击编辑器左侧边栏的折叠和展开按钮对代码块进行折叠和展开操作,还可以对选中的代码块单击鼠标右键选择折叠方式,包括折叠、递归折叠、全部折叠等操作。如图10所示。
图10 代码折叠
2.1.5. 代码快速注释
支持对选择的代码块进行快速注释,使用快捷键Ctrl+/ (macOS为Command+/ )进行快速注释。对于已注释的代码块,再次使用快捷键Ctrl+/ (macOS为Command+/ )取消注释。如图11所示。
图11 代码注释
2.1.6. 代码结构树
使用快捷键Alt + 7 / Ctrl + F12(macOS为Command+7)打开代码结构树,快速查看文件代码的结构树,包括全局变量和函数,类成员变量和方法等,并可以跳转到对应代码行。如图12所示。
图12 代码结构树
2.1.7. 代码引用查找
提供Find Usages代码引用查找功能,帮助开发者快速查看某个对象(变量、函数或者类等)被引用的地方,用于后续的代码重构,可以极大的提升开发者的开发效率。
使用方法:在要查找的对象上,单击鼠标右键 > Find Usages或使用快捷键Alt +F7(macOS为Option + F7)。可点击图标查看变量赋值位置,点击
图标查看变量引用情况。如图13所示。
图13 代码引用查找
2.1.8. 函数注释生成
DevEco Studio支持在函数定义处,快速生成对应的注释。在函数定义的代码块前,输入 “/”+回车键**,快速生成注释信息。如图14所示。
图14 函数注释生成
2.1.9. 代码查找
通过对符号、类或文件的即时导航来查找代码。检查调用或类型层次结构,轻松地搜索工程里的所有内容。通过连续点击两次Shift快捷键,打开代码查找界面,在搜索框中输入需要查找内容,下方窗口实时展示搜索结果。双击查找的结果可以快速打开所在文件的位置。如图15所示。
图15 代码查找
2.1.10. 快速查阅API接口及组件参考文档
在编辑器中调用ArkTS/JS API或组件时,支持在编辑器中快速、精准调取出对应的参考文档。
可在编辑器中,鼠标悬停在需要查阅的接口或组件,弹窗将显示当前接口/组件在不同API版本下的参数等信息,单击弹窗右下角Show in API Reference,或选中接口或组件,右键点击Show in API Reference,可以快速查阅更详细的API文档。如图16、图17所示。
图16 查阅API参考入口
图17 快速查阅API
2.1.11. Optimize Imports功能
使用编辑器提供的Optimize Imports,可以快速清除未使用的import,并根据设置的规则对import进行合并或排序。选择文件或目录,使用快捷键Ctrl+Alt+O(macOS为Control+Option+O),或单击菜单栏Code > Optimize Imports。
2.2. 代码生成/补全
2.2.1. 代码自动补全
提供代码的自动补全能力,编辑器工具会分析上下文,并根据输入的内容,提示可补全的类、方法、字段和关键字的名称等,支持模糊匹配。
自动补齐功能默认按最短路径进行排序,如仅需按照最近使用过的类、方法、字段和关键字等名称提供补全内容排序,可以在File > Settings(MacOS为DevEco Studio > Preferences) > Editor > General > Code Completion 中勾选“Sort suggestions by recently used”。
2.2.2. 快速覆写父类
DevEco Studio提供Override Methods,辅助开发者根据父类模板快速生成子类方法,提升开发效率。将光标放于子类定义位置,使用快捷键Ctrl+O,或右键单击Generate...,选择Override Methods,指定需要覆写的对象(方法、变量等),点击OK将自动生成该对象的覆写代码。
2.2.3. 快速生成构造器
编辑器支持为类快速生成一个对应的构造函数。
在类中使用快捷键Alt+Insert,或单击鼠标右键选择Generate...,在弹窗中选择Constructor,选择一个或多个需要生成构造函数的参数,点击OK。若选择Select None,则生成不带参数的构造器。
2.2.4. 快速生成get/set方法
编辑器支持为类成员变量或对象属性快速生成get和set方法。
将光标放置在当前类中,单击右键选择Generate...>Getter and Setter,或者使用快捷键Alt+Insert,在菜单中选择Getter and Setter,完成方法快速生成。
2.3. 代码实时检查及快速修复
2.3.1. 实时检查
编辑器会实时的进行代码分析,如果输入的语法不符合编码规范,或者出现语义语法错误,将在代码中突出显示错误或警告,将鼠标放置在错误代码处,会提示详细的错误信息。如图18所示。
从DevEco Studio 4.0 Release版本开始,当compatibleSdkVersion≥10时,编辑器代码实时检查支持ArkTS性能语法规范检查。
图18 实时检查
2.3.2. 代码快速修复
DevEco Studio支持代码快速修复能力,辅助开发者快速修复ArkTS代码问题。
查看告警信息: 使用双击Shift快捷键打开文件查询框,输入problems打开问题工具面板;双击对应告警信息,可以查看告警的具体位置及原因。
快速修复: 将光标放在错误告警的位置,可在弹出的悬浮窗中查看问题描述和对应修复方式;单击More actions可查看更多修复方法。或是在页面出现灯泡图标时,可点击图标并根据相应建议,实现代码快速修复。如图19所示。
图19 快速修复
2.4. 代码重构
2.4.1. Refactor-Extract代码提取
在编辑器中支持将函数内、类方法内等区域代码块或表达式,提取为新方法/函数(Method)、常量(Constant)、接口(Interface)、变量(Variable)或类型别名(Type Alias)。准确便捷的将所选区域代码从当前作用域内进行提取,提升编码效率。选中所需要提取的代码块,右键单击Refactor,选择需要提取的类型。
2.4.2. Refactor-Rename代码重命名
代码编辑支持Rename功能,可以快速更改变量、方法、对象属性等相关标识符及文件、模块的名称,并同步到整个工程中对其进行引用的位置。
使用方式:选中需要重新命名的标识符(变量、类、接口、自定义组件等),右键单击Refactor,选择Rename...(或使用快捷键Shift+F6),在弹框中输入新的标识符名称,并在Scope中选择替换的范围,点击Refactor完成重新命名。
代码编辑支持筛选并过滤不需要rename的引用位置。在Rename...弹窗中点击Preview,在弹出预览窗口中,用户选中无需Rename的选项,单击右键菜单Exclude/Remove进行过滤/删除,完成筛选后点击左下角Do Refactor,重新执行Rename操作。
2.4.3. Move File
在文件中单击右键,选择Refactor > Move File...,在弹窗中输入或点击...选择指定的目录,点击Refactor,可将当前文件移动至该目录下。勾选Search for references,可查找并更新工程中对该文件的引用;勾选Open in editor,可在编辑器中查看移动的文件。
2.4.4. Safe Delete
编辑器支持Safe Delete功能,帮助您安全地删除代码中的标识符对象(变量、函数或类等)或删除指定文件。在删除前,编辑器将先在代码中搜索对该对象的引用,如果存在引用,编辑器将提示您进行必要的检查和调整。
使用方式:在编辑器内选中需要删除的标识符对象或在工程目录选择待删除的文件,右键单击Refactor,选择Safe Delete,单击OK将自动检查当前对象在代码中被引用的情况,点击View Usages可查看具体使用的代码内容,点击Delete Anyway将直接删除该对象的定义。
2.5. 生成ArkTSDoc文档
- 在菜单栏选择Tools > Generate ArkTSDoc... 进入ArkTSDoc生成界面。如图20所示。
- 设置生成ArkTSDoc的范围,可选择整个工程、某个模块或目录、单个文件进行导出。在Output directory中指定导出ArkTSDoc的存储路径。
图20 生成ArkTSDoc文档入口
- 若勾选Open generated documentation in browser选项,在生成ArkTSDoc后,将自动打开相应页面查看生成的文档。配置完毕后点击Generate,开始扫描并生成ArkTSDoc文档。
生成的ArkTSDoc左侧文档目录和原工程目录结构一致,右侧可点击跳转到当前文件包含的某个变量、方法、接口或类的文档位置。如图21所示。
图21 生成文档
若没有勾选Open generated documentation in browser选项,在生成ArkTSDoc后,DevEco Studio右下角弹出对应提示框,可以点击Go to Folder跳转到生成的ArkTSDoc文件夹,用浏览器打开文件夹中index.html文件即可查看ArkTSDoc文档。
--未完待续--
本文配套视频教程观看地址:
04-代码编辑-代码生成补全、代码检查、代码重构和生成ArkTSDoc文档
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接
HarmonyOS应用开发基础案例(一):鸿蒙页面布局入门
本文以仿猫眼电影M站首页布局为案例,展示ArkUI在实际开发中的应用。内容包括案例效果及相关知识点,深入解析布局框架以及头部、脚部、内容区域的构建思路与代码实现,最后提供完整代码和教程资源,助力你强化实践能力。
1. 案例效果截图
如图1-1所示。
图1-1 案例效果截图
2. 案例运用到的知识点
- 核心知识点
- UI范式基本语法。
- 文本显示Text、Span组件。
- 线性布局Column、Row组件。
- 层叠布局Stack组件。
- 按钮Button组件。
- 显示图片Image组件。
- 其他知识点
- DevEco Studio的基本使用。
- 简单的资源分类访问。
- 移动端APP布局基本技巧。
3. 布局框架
可以按照图3-1来思考布局的框架:
图3-1 布局框架图
框架的代码如下:
@Entry
@Component
struct Index {
build() {
Column() {
Stack() {}
.width('100%').height(50).backgroundColor('#e54847')
Column() {
Text('content')
}.width('100%').layoutWeight(1)
Row() {}
.width('100%').height(50).backgroundColor('#fff')
.border({width: { top: 1}, color: '#eee'})
}
}
}
4. 头部区域
可以按照图4-1思路来构建布局:
图4-1 布局示意图
代码如下:
// 头部区域
Stack({ alignContent: Alignment.End }) {
Text('猫眼电影')
.width('100%').height('100%').textAlign(TextAlign.Center)
.fontColor('#fff').fontSize(18)
Image($rawfile('menu.png'))
.width(17).height(16).margin({ right: 10 })
}.width('100%').height(50).backgroundColor('#e54847')
5. 脚部区域
可以按照图5-1思路来构建布局:
图5-1 布局示意图
代码如下:
// 脚部区域
Row() {
Column() {
Image($rawfile('movie.svg'))
.width(25).height(25).fillColor('#e54847')
Text('电影/影院')
.fontSize(10).fontColor('#e54847')
}.layoutWeight(1).height('100%').justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('video.png'))
.width(25).height(25).fillColor('#696969')
Text('视频')
.fontSize(10).fontColor('#696969')
}.layoutWeight(1).height('100%').justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('perform.svg'))
.width(25).height(25).fillColor('#696969')
Text('演出')
.fontSize(10).fontColor('#696969')
}.layoutWeight(1).height('100%').justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('mine.svg'))
.width(25).height(25).fillColor('#696969')
Text('我的')
.fontSize(10).fontColor('#696969')
}.layoutWeight(1).height('100%').justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%').height(50).border({ width: { top: 1 }, color: '#eee' })
.backgroundColor('#fff')
6. 内容区域
可以参照图6-1思考内容区域的整体框架布局:
图6-1 布局示意图
代码如下:
// 内容区域
Column() {
Row() {}
.width('100%').height(44).border({width: {bottom: 1}, color: '#e6e6e6'})
Scroll() {}
.layoutWeight(1)
}.width('100%').layoutWeight(1)
6.1. 导航区
内容区域的导航区可以参照图6-2思考布局:
图6-2 布局示意图
代码如下:
// 导航区
Row() {
Row() {
Text('北京').fontColor('#666')
Text('')
.width(0).height(0)
.border({
width: 5,
color: {
top: '#b0b0b0',
left: Color.Transparent,
right: Color.Transparent,
bottom: Color.Transparent
}
})
.margin({top: 6, left: 4})
}.offset({x: 15}).width(60)
Row() {
Stack() {
Text('热映')
.fontSize(17).fontWeight(FontWeight.Bold)
Text('')
.width(24).border({width: {bottom: 3}, color: '#e54847'}).offset({y: 18})
}
Text('影院')
Text('待映')
Text('经典电影')
}.justifyContent(FlexAlign.SpaceEvenly).layoutWeight(1)
Row() {
Image($rawfile('search-red.png'))
.width(20).height(20)
}.justifyContent(FlexAlign.Center).width(50)
}.width('100%').height(44).border({width: {bottom: 1}, color: '#e6e6e6'})
6.2. 最受好评区
可以参照图6-3考虑整体布局:
图6-3 布局示意图
代码如下:
// 好评和列表区内容
Column() {
// 好评区
Column() {
Text('最受好评电影')
.width('100%').fontSize(14).fontColor('#333')
.textAlign(TextAlign.Start).margin({ bottom: 12 })
Scroll() {
Row() {
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/'
+ '54ecdeddf2a92339dd2c95022e99e5fe27091.jpg?'
+ 'imageMogr2/thumbnail/2500x2500%3E'
).width(85).height(115)
Text('')
.width('100%').height(35).linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00').fontSize(11)
.fontWeight(700).offset({ x: 4, y: -4 })
}.height(115).margin({ bottom: 6 })
Text('出走的决心')
.fontSize(13).fontWeight(FontWeight.Bold).width(85)
.textAlign(TextAlign.Start).margin({ bottom: 3 })
}.width(85).margin({ right: 10 })
// ... 其余7个Column与上述结构相同,因篇幅限制已省略,详见本书配套源码。
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}.width('100%').padding({ top: 12, bottom: 12, left: 15, right: 15 })
}.height('100%')
6.3. 列表区
列表区整体布局参照图6-4。
图6-4 布局示意图
代码如下:
// 列表区
Column() {
Row() {
Image('https://p0.pipi.cn/basicdata/'
+ '54ecdedd5377e187a9e7aa5ee9ec15a184b18.jpg?'
+ 'imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180'
).width(64).height(90)
Stack({ alignContent: Alignment.End }) {
Column() {
Row() {
Text('志愿军:存亡之战').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('274337').fontColor('#faaf00')
Span('人想看').fontColor('#666').fontSize(13)
}
Text('主演: 朱一龙,辛柏青,张子枫').fontColor('#666').fontSize(13)
Text('2024-09-30 下周一上映').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start).height('100%').width('100%')
.justifyContent(FlexAlign.SpaceEvenly).padding({ right: 53 })
Button() {
Text('预售').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#3C9FE6')
}
.height('100%').layoutWeight(1).margin({ left: 12 })
.padding({ top: 12, right: 14, bottom: 12, left: 0 })
.border({ width: { bottom: 1 }, color: '#eee' })
}.height(144)
// ... 其余7个Row与上述结构相同,因篇幅限制已省略,详见本书配套源码。
}.backgroundColor('#fff').padding({ left: 15 })
--THE END--
本文配套视频教程观看地址:
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接
TS和JS成员变量修饰符
在 TypeScript 和 JavaScript 中,类成员变量(属性)的修饰符(Modifiers) 用于控制其可见性、可访问性和可变性。两者在能力上有显著差异:TypeScript 提供了更丰富的编译时修饰符,而 JavaScript(ES2022 起)引入了运行时私有字段。
下面从 TypeScript 和 JavaScript 两个角度分别说明,并对比异同。
一、TypeScript 类成员变量修饰符(编译时)
TypeScript 在 编译阶段 提供以下关键字作为访问修饰符:
| 修饰符 | 含义 | 是否生成 JS 代码 | 可见性 |
|---|---|---|---|
public |
公共(默认) | ❌ 不生成额外代码 | 类内、子类、外部均可访问 |
private |
私有 | ❌ 仅类型检查,不阻止运行时访问 | 仅类内部可访问(TS 编译时报错) |
protected |
受保护 | ❌ 仅类型检查 | 类内部 + 子类可访问 |
readonly |
只读 | ❌ 仅类型检查 | 初始化后不可修改(TS 报错) |
✅ 示例(TypeScript):
class User {
public name: string; // 默认就是 public
private id: number; // TS 禁止外部访问
protected email: string; // 子类可访问
readonly createdAt: Date; // 初始化后不可改
constructor(name: string, id: number, email: string) {
this.name = name;
this.id = id;
this.email = email;
this.createdAt = new Date();
}
}
⚠️ 注意:
private/protected只在 TypeScript 编译时生效,编译成 JS 后,这些字段仍是普通属性,运行时仍可被访问或修改!
// 编译后的 JS(无 private 保护!)
const user = new User("Alice", 1, "a@example.com");
console.log(user.id); // ✅ 能访问!JS 不报错
user.id = 999; // ✅ 能修改!
二、JavaScript 类成员变量修饰符(运行时,ES2022+)
从 ECMAScript 2022(ES13) 开始,JavaScript 原生支持 真正的私有字段(Private Fields) ,使用 # 前缀。
| 语法 | 含义 | 运行时是否私有 | 是否可被外部访问 |
|---|---|---|---|
#fieldName |
私有字段 | ✅ 是 | ❌ 完全无法从类外访问 |
| 普通字段(无前缀) | 公共字段 | ❌ 否 | ✅ 可自由访问 |
✅ 示例(JavaScript / TypeScript 均支持):
class User {
#id; // 私有字段(JS 原生私有)
name; // 公共字段
constructor(name, id) {
this.name = name;
this.#id = id; // 只能在类内部访问
}
getId() {
return this.#id; // ✅ OK
}
}
const user = new User("Bob", 2);
console.log(user.name); // ✅ "Bob"
console.log(user.#id); // ❌ SyntaxError! 无法访问
✅ 关键优势:
#id是真正的私有,即使在运行时也无法绕过(除非用 Proxy 等 hack,但正常代码做不到)。
三、TypeScript 对 JS 私有字段的支持
TypeScript 完全支持 # 私有字段,并提供类型检查:
class User {
#id: number;
name: string;
constructor(name: string, id: number) {
this.name = name;
this.#id = id;
}
getId(): number {
return this.#id; // ✅ TS 知道这是 number
}
}
🔸 此时你不需要用
private,因为#id已经是运行时私有。
四、对比总结
| 特性 | TypeScript private
|
JavaScript #field
|
|---|---|---|
| 作用时机 | 编译时(类型检查) | 运行时(真实私有) |
| 能否被外部访问 | ✅ 能(JS 无保护) | ❌ 不能 |
| 是否生成额外代码 | ❌ 否 | ✅ 是(保留 # 语法) |
| 兼容性 | 所有 JS 环境(因被擦除) | 需要 ES2022+ 或 Babel 转译 |
| 推荐场景 | 快速开发、内部项目 | 需要真正封装、库开发 |
五、最佳实践建议
✅ 优先使用 JavaScript 原生私有字段 #
- 如果目标环境支持(现代浏览器 / Node.js 12+),优先用
#fieldName。 - 它提供真正的封装,避免“假装私有”的陷阱。
✅ 在 TypeScript 中:
- 若需兼容旧环境 → 用
private(但要清楚它只是“纸面私有”)。 - 若用现代环境 → 直接用
#,无需private。
✅ 不要混用:
// ❌ 不推荐:语义重复且混乱
private #id; // 错误!不能同时用
✅ readonly 仍是 TS 特有(JS 无等价物)
-
可配合
#使用:class Config { readonly #apiUrl: string; constructor(url: string) { this.#apiUrl = url; // 初始化后不可变(TS 检查) } }
六、补充:其他相关修饰符
| 修饰符 | 语言 | 说明 |
|---|---|---|
static |
TS & JS | 静态成员(属于类,不属于实例) |
abstract |
TS only | 抽象类/方法(不能实例化) |
declare |
TS only | 声明属性存在(用于 .d.ts 或装饰器) |
✅ 总结
| 需求 | 推荐方案 |
|---|---|
| 真正的私有字段 | 使用 JavaScript #fieldName(ES2022+) |
| 仅开发时提醒(兼容旧环境) | 使用 TypeScript private
|
| 只读属性 | TypeScript readonly(JS 无原生支持) |
| 公共字段 | 直接声明(TS/JS 均默认 public) |
🎯 现代项目建议:
用#实现私有,用readonly实现只读,放弃private(除非必须兼容旧 JS) 。
这样既能获得类型安全,又能保证运行时封装性。
前端进阶系列之《浏览器渲染原理》
想搞懂浏览器是怎么变魔术,把代码变成眼前漂亮页面的吗?🎩✨ 这次我们就以 www.baidu.com/ 为例,一起揭开这场视觉魔术的幕后秘密!从前端代码到流畅的用户界面,每一步都藏着超有意思的细节,也是我们打造高性能前端应用的核心基石~快跟上我,一起探索这场奇妙的页面诞生记吧!🚀
当我们打开 www.baidu.com/ 如何渲染出下面的界面
以 Chrome 浏览器为例
1、浏览器的多进程架构
首先浏览器会分配几个进程,页面的渲染是多个进程之间相互配合的结果
- 浏览器进程:主要负责界面显示、用户交互,同时提供存储等功能
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,包含排版引擎 Blink、V8 JS 引擎,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程
- 网络进程:主要负责页面的网络资源加载
- GPU 进程:只有一个 GPU 进程,为所有浏览器 Tab 标签页服务。它负责与 GPU 硬件直接通信,执行页面光栅化和合成操作
- Spare Renderer(Chrome 备用渲染进程): 是一个在后台提前准备好的、空闲的网页渲染进程,目的是提升用户体验和浏览速度而采用的一种优化策略
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离
页面的渲染需要多进程之间相互搭配,重点在于浏览器进程、渲染进程、网络进程、GPU进程之间的配合
2、HTTP 请求过程
浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:
-
构建请求:
GET https://www.baidu.com/ HTTP2 -
查找缓存:当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载
* 缓存方式:强制缓存/协商缓存 200 304 Cache-Control/Last-Modified/ETag
* 缓存分类:Memory Cache/Disk Cache/Service Worker/Push Cache(HTTP 2)
- 准备 IP 和端口:请求 DNS 系统返回域名对应的 IP、端口
www.baidu.com:170.114.52.116:443
-
等待 TCP 队列:同一个域名同时最多只能建立 6 个 TCP(http1.1) 连接,其他请求进入排队等待
-
建立 TCP 连接:三次握手,客户端和服务器总共要发送三个数据包以确认连接的建立
-
发起 HTTP 请求:HTTP 请求行、请求头、请求体
-
服务器处理请求/服务器返回请求:返回响应行、响应头、响应体
-
断开连接:执行"四次挥手"来保证双方都能断开连接
3、从输入 URL 到页面展示
整个过程需要各个进程之间的配合,如下图所示
-
用户输入:浏览器进程判断输入的关键字是搜索内容,还是请求的 URL,整合成完整的 URL
-
URL 请求过程:浏览器进程会通过 IPC 把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会发起请求,执行步骤二。这里补充几个重要的知识点
-
响应状态码:301/302 200 304 400 500…
-
响应类型(Content-Type):服务器返回的响应体数据是什么类
1. text/html:服务器返回的数据是 HTML 格式,触发渲染流程 2. application/octet-stream:数据是字节流类型的,触发下载流程 3. application/json:JSON 字段,前后端常见类型 4. text/css|text/javascript |application/javascript:CSS 文件、JS 文件 -
-
准备渲染进程:网络进程将响应信息传递给浏览器进程,如果是 text/html,则渲染进程准备接收数据
-
提交文档:文档可以理解为响应体即返回的 HTML 内容
- 所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程
- 渲染进程接收到"提交文档"的消息后,会和网络进程建立传输数据的"管道"
-
渲染阶段:当渲染进程接收到网络进程的响应数据时,便开始页面解析和子资源加载了,边加载边解析
接下来就进入到另外一个部分,渲染进程将网络进程接收到的 HTML + CSS + JS 生成页面,如下图所示
4、页面渲染流程 - 渲染流水线
当网络进程和渲染进程建立管道链接时,渲染进程便可以接收 HTML CSS JS 等数据,接下来渲染进程开始渲染工作。但渲染机制过于复杂,在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段显示成页面,这样的处理流程叫做渲染流水线
按照渲染的时间顺序,流水线可分为如下几个子阶段:
构建 DOM 树、样式计算、布局阶段、分层、绘制、栅格化和合成
4.1 构建 DOM 树
浏览器无法直接理解和使用服务器返回的 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树, HTML 经过 HTML 解析器解析,生成树状结构的 DOM
4.2 样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成
- 把 CSS 转换为浏览器能够理解的结构:styleSheets
- 转换样式表中的属性值,使其标准化
- 计算出 DOM 树中每个节点的具体样式:继承规则和层叠规则
4.3 布局阶段
现在我们有 DOM 树和 DOM 树中元素的样式,接下来需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局
创建布局树:只包含可见元素的的布局树
- 渲染进程遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
- 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容、display:none
布局计算:计算布局树的几何位置
到这里我们完成了渲染流程的前三个阶段:DOM 生成、样式计算和布局,像下图一样
4.4 分层
我们看到的界面都是多个图层相互叠加的结果,类似于 PS 的图层一样
position:fixed、z-indexing 做 z 轴排序都会有新的图层
渲染引擎需要为节点生成专用的图层,并生成一棵对应的图层树(LayerTree),具有几个特点
- 如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
- 拥有层叠上下文属性的元素会被提升为单独的一层
4.5 图层绘制
渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示
- 绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等
- 而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制
- 所以在图层绘制阶段,产生的内容就是这些待绘制列表
4.5 栅格化(复杂)
新的名称:合成线程
合成线程 是渲染进程中独立于主线程的一个关键工作线程,它的主要目标是高效地将页面的不同部分合成最终的图像,并发送给GPU进行绘制,从而确保滚动、动画等操作极其流畅
第 1 步:当图层的绘制列表准备好之后,主线程会把图层绘制列表提交给合成线程
两个概念
- 视口:通常页面很长,用户能看到的部分叫做视口
- 图块:合成线程会将图层划分为图块,这些图块的大小通常是 256 * 256 px 或者 512 * 512 px
第 2 步:合成线程会为每个图块生成对应的光栅化任务
- 这个任务包含了"图块的位置、图块所属图层的那部分绘制记录" 的信息
- 由渲染进程的栅格化线程执行,优先处理视口附近的图块
第 3 步:提交任务到 GPU 进程
- 栅格化线程将这些光栅化任务以及相关的绘制记录(作为数据源)放入一个队列中
- 然后,它通过 IPC 将任务队列提交给 GPU 进程。进行 GPU加速
第 4 步:GPU 进程与 GPU 硬件执行光栅化
- GPU 进程接收任务:GPU 进程收到来自渲染进程(合成线程)的任务队列。
- 进行光栅化操作
什么是光栅化?
简单说,就是将矢量图形(如绘制记录中的矩形、路径、文字命令)转换为屏幕上的像素(位图)的过程。GPU 进程会驱动 GPU 硬件来执行实际的光栅化工作。它利用 GPU 上强大的、高度并行化的光栅化器来高效完成此任务
- 结果存储:光栅化后的每个图块存储在 GPU 的显存中
4.6 合成和显示
第 1 步:生成绘制(DrawQuad)指令
- 当所有(或优先的)图块都光栅化完成后,合成线程会收集每个图块在 GPU 显存中的位置信息(ID)
第 2 步:和浏览器进程通信
- 浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的合成图像
- 然后浏览器进程和 GPU 进程通信,发送合成帧
第 3 步:GPU 进程驱动合成
- GPU 进程接收到合成帧后,会向 GPU 发送最终的绘制命令
- GPU 根据合成帧的指令,将各个图块纹理(就像一张张图片)绘制到屏幕缓冲区的正确位置上
以上这个过程就是合成 ,它通常通过非常高效的硬件加速操作(如纹理映射)来完成
显示:最终,屏幕的显示控制器从帧缓冲区读取数据,并将像素点亮,用户就看到了最终的页面
结合上图,一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式
- 创建布局树,并计算元素的布局信息
- 对布局树进行分层,并生成分层树
- 为每个图层生成绘制列表,并将其提交到合成线程
- 合成线程将图层分成图块,并在光栅化线程池中配合 GPU 将图块转换成位图
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
5、拓展内容
5.1 页面的重新渲染(重排、重绘)
来介绍以下前端常说的渲染流水线中两个概念,重排、重绘
1、重排:更新元素的几何位置
例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的
常见的引起重排的方式
-
页面首次渲染
-
浏览器窗口大小发生变化
-
元素的内容发生变化
-
元素的尺寸或者位置发生变化
-
元素的字体大小发生变化
-
添加或者删除可见的DOM元素
2、重绘:更新元素的绘制属性
比如通过 JavaScript 更改某些元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些
常见的引起重绘的方式
- 改变颜色 (
color,background-color) - 改变可见性 (
visibility) - 改变边框样式(如
border-style,但不改变border-width) - 文本阴影 (
text-shadow)
5.2 性能优化
分为两个阶段:加载阶段和交互阶段、关闭阶段
加载阶段
降低关键资源个数
降低关键资源大小:压缩代码、去除代码注释等
降低关键资源需要多少个 RTT:使用 CDN、缓存
交互阶段
前端性能优化之“代码分割与懒加载”)
在现代前端开发中,随着单页面应用(SPA)的复杂度不断提升,打包后的JavaScript文件体积可能达到几MB甚至更大。这会导致严重的性能问题:用户需要等待整个应用加载完成后才能与页面交互。代码分割(Code Splitting)和懒加载(Lazy Loading)正是解决这一问题的关键技术。
一、代码分割的核心思想
代码分割的核心是将单个大型的打包文件拆分成多个较小的chunk,然后按需加载或并行加载。这类似于图书的章节划分——读者不需要一次性拿到整本书,而是可以根据需要阅读特定章节。
二、实现方式
- 动态import()语法(现代推荐方案)
// 静态导入(传统方式)
// import { utils } from './module';
// 动态导入(代码分割)
button.addEventListener('click', async () => {
const module = await import('./module.js');
module.doSomething();
});
Webpack等构建工具检测到动态import()时会自动进行代码分割,生成独立的chunk文件。
- React.lazy + Suspense(React生态)
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
- 路由级分割(最常用的分割场景)
// Vue Router
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
];
// React Router
const Dashboard = lazy(() => import('./Dashboard'));
三、懒加载的实际应用场景
- 路由级别分割:每个路由对应的组件单独打包
- 组件级别分割:大型组件(如富文本编辑器、图表库)按需加载
- 第三方库分割:将不常变化的第三方库单独打包
// webpack配置示例
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
}
}
}
}
四、性能收益分析
通过代码分割可以实现:
· 减少初始加载时间:首屏只需加载必要代码 · 提高缓存效率:修改业务代码不会影响vendor chunk · 优化资源加载:非关键资源可以延迟加载
五、最佳实践建议
- 分割粒度要合理,避免产生过多小文件导致请求频繁
- 使用预加载(preload/prefetch)优化用户体验
<link rel="prefetch" href="lazy-component.js">
- 注意错误处理,动态导入可能失败
import('./module')
.catch(() => {
// 处理加载失败情况
});
代码分割不是银弹,需要结合实际业务场景。过度分割可能导致请求碎片化,而分割不足又无法充分发挥性能优势。通过Chrome DevTools的Coverage工具和Webpack Bundle Analyzer进行分析,找到最适合自己项目的分割策略才是关键。
深入理解内容安全策略(CSP):原理、作用与实践指南
一、CSP 是什么?
CSP 本质上是一套由网站开发者定义、浏览器负责执行的 “内容加载规则” 。它通过明确告诉浏览器 “哪些来源的资源(如脚本、图片、样式表)是安全的,可以加载;哪些来源是危险的,必须拒绝”,从根源上限制恶意代码的执行,弥补传统安全防护(如输入过滤、输出编码)的不足。
1. CSP 的核心逻辑:“白名单机制”
CSP 的核心思想是 “默认拒绝,例外允许”—— 除非开发者明确将某个资源来源加入 “白名单”,否则浏览器会默认阻止该资源的加载或执行。这种机制彻底改变了传统网页 “默认允许所有资源” 的宽松模式,大幅降低了恶意资源注入的风险。
2. CSP 的实现载体:两种部署方式
CSP 的规则通常通过以下两种方式传递给浏览器,开发者可根据场景选择:
-
HTTP 响应头(推荐) :在服务器返回的 HTTP 响应中添加
Content-Security-Policy头,例如:Content-Security-Policy: default-src 'self'; script-src https://cdn.example.com。这种方式优先级高、规则覆盖全面,适用于整站或特定页面的安全管控。 -
<meta>标签:众所周知可以通过<meta>标签来模拟响应头,例如:<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://img.example.com">。这种方式无需服务器配置,适合静态页面或无法修改服务器响应头的场景(如静态博客、第三方平台托管页面)。
二、CSP 的核心作用 —— 抵御哪些威胁?解决什么问题?
CSP 的核心价值在于 “精准管控资源加载,阻断恶意代码执行”,具体可拆解为以下 4 个关键作用:
1. 抵御 XSS 攻击:从 “事后过滤” 到 “事前阻止”
XSS 攻击的本质是恶意脚本被注入到网页中并执行,而 CSP 通过限制 “脚本的加载来源” 和 “脚本的执行方式”,直接切断恶意脚本的执行路径:
- 禁止加载未知来源的脚本:例如通过
script-src 'self' https://cdn.tencent.com,仅允许加载网站自身('self')和腾讯 CDN 的脚本,恶意网站的脚本会被直接拦截; - 禁止内联脚本(如
<script>alert(1)</script>)和eval()函数:内联脚本是 XSS 攻击的常用载体,CSP 默认拒绝内联脚本(需显式配置'unsafe-inline'才允许,且不推荐),同时禁用eval()等动态执行脚本的函数,彻底杜绝 “动态注入恶意代码” 的可能。
2. 防止数据注入与恶意资源加载
除了脚本,CSP 还能管控图片、样式表、字体、iframe 等各类资源的加载来源:
- 例如通过
img-src https://img.example.com data:,仅允许加载指定域名的图片和data:协议的 Base64 图片,避免攻击者注入恶意图片(如包含恶意代码的 SVG); - 通过
frame-src 'none'禁止加载任何 iframe,防止 “点击劫持”(Clickjacking)攻击 —— 即攻击者通过 iframe 嵌套合法网站,诱导用户点击恶意按钮。
3. 增强网站的 “安全透明度”:报告机制
CSP 支持 “违规报告” 功能:当浏览器检测到违反 CSP 规则的行为时(如尝试加载未授权的脚本),会自动向开发者指定的 “报告地址” 发送 JSON 格式的日志,包含违规资源的 URL、违规类型、触发页面等信息。
这一机制让开发者能实时监控网站的安全风险,例如:
- 发现未被注意的第三方脚本加载(可能存在安全隐患);
- 验证 CSP 规则是否配置正确(避免误拦截合法资源)。
4. 兼容 “渐进式安全”:从测试到强制
为了避免直接启用 CSP 导致合法资源被拦截(影响网站功能),CSP 提供了 “测试模式”:将 HTTP 响应头改为Content-Security-Policy-Report-Only,此时浏览器仅会记录违规行为(发送报告),但不会实际阻止资源加载。
三、CSP 的具体使用 —— 规则配置、常见场景与注意事项
CSP 的使用核心是 “配置规则”,而规则由 “指令(Directive)” 和 “源(Source)” 两部分组成。下面从基础配置逻辑、常见场景示例、避坑指南三个方面,讲解 CSP 的实际应用。
1. 基础:CSP 的 “指令” 与 “源” 详解
(1)核心指令:管控不同类型的资源
CSP 提供了数十种指令,覆盖各类资源和行为,以下是最常用的 8 个指令:
| 指令(Directive) | 作用 | 示例 |
|---|---|---|
default-src |
所有资源的 “默认加载规则”,当其他指令(如script-src)未配置时,会继承default-src的规则 |
default-src 'self'(默认仅允许加载自身域名资源) |
script-src |
管控 JavaScript 脚本的加载与执行(最关键的指令之一) | script-src 'self' https://cdn.jsdelivr.net |
style-src |
管控 CSS 样式表的加载与执行 | style-src 'self' https://fonts.googleapis.com |
img-src |
管控图片资源(jpg、png、svg 等)的加载 | img-src 'self' https://img.baidu.com data: |
font-src |
管控字体文件(woff、ttf 等)的加载 | font-src 'self' https://fonts.gstatic.com |
frame-src |
管控 iframe 嵌套的来源 |
frame-src 'none'(禁止所有 iframe) |
report-uri / report-to
|
指定违规报告的接收地址(report-uri是旧标准,report-to是新标准,建议同时配置) |
report-uri /csp-report; report-to csp-endpoint |
object-src |
管控插件资源(如 Flash、Java Applet,现在较少使用) |
object-src 'none'(禁止所有插件) |
(2)常见 “源” 值:定义 “安全的资源来源”
“源” 是指令后紧跟的 “允许加载的资源地址”,支持多种格式,以下是最常用的 5 种:
| 源(Source) | 含义 | 示例 |
|---|---|---|
'self' |
允许加载当前网站的同源资源(同协议、同域名、同端口) | default-src 'self' |
| 完整域名 | 允许加载指定域名的资源(可带端口,默认 80/443) | script-src https://cdn.example.com:8443 |
| 通配符域名 | 允许加载指定域名下的所有子域名(如*.example.com) |
img-src *.example.com |
data: |
允许加载 Base64 编码的资源(如data:image/png;base64,...) |
img-src data: |
'none' |
禁止加载该类型的所有资源 | frame-src 'none' |
⚠️ 注意:'self'、'none'、'unsafe-inline'等带引号的值,必须使用单引号包裹,否则浏览器会将其识别为 “域名”(如self会被当作self.com),导致规则失效。
HTML 敲击乐 PART--2
HTML5 敲击乐项目 PART-2:从结构到交互的完整实现
一、项目概述
“HTML5 敲击乐”是一个基于现代 Web 技术的交互式音频应用:用户按下键盘上的特定按键(如 A、S、D 等),页面中对应的虚拟“打击乐器键”会高亮显示并播放预设音效,模拟真实的节奏打击体验。
该项目不仅实现了基础的音效触发与视觉反馈,更完整体现了前端开发的核心理念——结构、样式与行为分离,并融合了响应式设计、模块化思维与用户体验优化等现代开发实践。代码结构清晰、扩展性强,为后续功能(如节奏录制、音色切换、移动端触控支持等)预留了良好接口,是初学者掌握 HTML5、CSS3 与 JavaScript 协同开发的理想范例。
跟着本文,你将亲手构建一个既美观又富有交互感的网页乐器!
二、HTML5 结构设计:语义化与数据绑定
HTML 是应用的骨架。我们采用语义化标签与数据属性(data-*) 构建清晰、可维护的 DOM 结构:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>HTML5 敲击乐</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="keys">
<div class="key" data-key="65">
<h3>A</h3>
<p class="sound">Clap</p>
</div>
<!-- 更多 .key 元素 -->
</div>
<audio data-key="65" src="sounds/clap.wav"></audio>
<!-- 对应的 audio 元素 -->
<script src="script.js"></script>
</body>
</html>
关键设计要点:
-
.keys容器:包裹所有虚拟琴键,作为布局根节点; -
.key模块:每个键包含视觉提示(<h3>)与音效名称(.sound),具备独立语义; -
data-key属性:存储键盘事件的keyCode(如 A 键为 65),实现 UI 元素与物理按键的精准映射; -
<audio>预加载:每个音效通过独立<audio>标签预加载,并通过相同的data-key与.key关联,便于 JavaScript 动态调用。
最佳实践:
- HTML 只负责结构,不嵌入样式或逻辑;
- CSS 通过外链引入,置于
<head>;- JavaScript 脚本置于
<body>底部,避免阻塞渲染。
三、CSS 样式系统:重置、布局与动效
1. 统一样式起点:CSS Reset
不同浏览器对默认元素(如 body、h1)的 margin、padding 等处理不一,易导致布局错乱。为此,项目采用 Eric Meyer’s Reset CSS,清除所有默认样式,并补充现代开发规范:
*, *::before, *::after {
box-sizing: border-box;
}
box-sizing: border-box 确保元素的宽高包含 padding 和 border,使尺寸计算更直观、布局更稳定。
此外,还优化了常用元素:
-
img { max-width: 100%; height: auto; }:图片自适应容器; -
a { text-decoration: none; color: inherit; }:链接样式继承父级,保持视觉统一。
2. 响应式布局:Flexbox + 相对单位
为适配手机、平板、桌面等多种设备,项目摒弃固定单位(如 px),转而使用:
-
vh(视口高度单位) :min-height: 100vh确保.keys容器始终占满屏幕高度; -
rem(根字体单位) :
设置html { font-size: 10px; },后续所有尺寸(如font-size: 1.5rem、border: .4rem)均基于此,便于全局缩放。
配合 Flexbox 弹性布局,实现优雅居中:
.keys {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
无论屏幕尺寸如何变化,琴键始终居中排列,布局自适应且无需媒体查询干预。
3. 视觉反馈:动态交互效果
当用户触发按键时,JavaScript 会为对应 .key 添加 .playing 类,CSS 则定义其动效:
.playing {
border-color: #ffc600;
box-shadow: 0 0 1rem #ffc600;
transform: scale(1.1);
transition: all 0.1s ease;
}
这一组合实现了边框高亮、发光阴影与轻微放大的视觉反馈,显著提升交互沉浸感。
4. 背景与氛围营造
全屏背景图增强整体氛围:
body {
background: url('./background.jpg') bottom center no-repeat;
background-size: cover;
}
-
cover:等比缩放覆盖整个视口(可能裁剪边缘); -
contain:完整显示图片(可能留白); -
bottom center:确保关键视觉内容(如鼓面)位于底部中央。
四、JavaScript 交互逻辑:事件驱动与 DOM 操作
JavaScript 是让页面“活起来”的引擎。项目采用事件监听 + 数据驱动的方式实现核心交互。
1. 安全执行时机
使用 DOMContentLoaded 确保 DOM 加载完成后再执行脚本:
document.addEventListener('DOMContentLoaded', () => {
// 初始化逻辑
});
避免因元素未就绪导致的 null 错误。
2. 全局键盘监听
监听 window 的 keydown 事件,捕获用户输入:
window.addEventListener('keydown', playSound);
3. 动态匹配与反馈
function playSound(event) {
const keyCode = event.keyCode;
const keyElement = document.querySelector(`.key[data-key="${keyCode}"]`);
const audioElement = document.querySelector(`audio[data-key="${keyCode}"]`);
if (keyElement) {
keyElement.classList.add('playing');
setTimeout(() => keyElement.classList.remove('playing'), 150); // 自动移除动效
}
if (audioElement) {
audioElement.currentTime = 0; // 重置播放位置
audioElement.play();
}
}
- 通过
data-key精准匹配 UI 与音频; - 使用
setTimeout自动清除.playing类,避免状态残留; - 重置
currentTime支持快速连按。
五、性能与工程化考量
✅ 最佳实践总结:
| 方面 | 实践 |
|---|---|
| 脚本加载 |
<script> 置于 </body> 前,避免阻塞渲染 |
| 选择器性能 | 避免通配符 * 重置,采用明确元素列表(如 Eric Meyer 方案) |
| 可访问性 | 使用语义化标签(<h3>、<p>),提升屏幕阅读器兼容性 |
| 扩展性 |
data-* 属性天然支持未来功能扩展(如音量控制、音色切换) |
六、结语:小项目,大格局
“HTML5 敲击乐”虽体量小巧,却完整涵盖了现代 Web 开发的关键要素:
- 结构清晰:语义化 HTML + 数据绑定;
- 样式健壮:CSS Reset + Flexbox + 响应式单位;
- 交互流畅:事件驱动 + 动态 DOM 操作 + 视觉反馈;
- 工程规范:职责分离、性能优化、可扩展设计。
它不仅是一次趣味编程实践,更是通往专业前端开发的坚实一步。掌握其中的设计思想与技术组合,你便已具备构建更复杂交互应用的能力。
下一步,不妨尝试添加:
- 移动端触控支持
- 节奏录制与回放
- 多套音效主题切换
让你的“敲击乐”真正成为属于你的数字乐器!