普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月8日技术

Cursor 在前端需求开发工作流中的应用|得物技术

作者 得物技术
2025年4月8日 09:45

一、引言

很高兴与大家分享现阶段 Cursor 在我的工作中的使用体验。首先是预期管理,本篇文章不会分享 x 个你可能不知道的小技巧,也不会让你拥有无需自行编码的能力,同时不涉及 Cursor 工程化方面内容。仅仅是围绕个人开发流程中的已有问题,分享如何使用 Cursor 来提升这部分的开发体验,在工作中持续保持好的节奏和状态。

TL;DR

  • 列举 Cursor 的错误预期

  • 相比过去的开发流程,使用 Cursor 后的变化

  • Cursor 在现状分析、方案设计和影响评估中的收益

二、就差一个程序员了

最近团队在大力推广 Cursor AI,随着几个版本迭代体验下来,其精准的自动补全深得我心,具体可以体现在 Tab 键的使用率已逐渐高于 Command + C/V。既然这么懂我,那么能否更进一步,根据 PRD 直接输出代码呢?

2.1 从需求到代码

Cursor 能够理解代码上下文,从而根据简短的描述生成符合上下文的代码,于是我尝试直接将 PRD 提供给 Cursor 来生成代码:

PRD → Cursor → Code(一步到位)

几个需求尝试下来,总的来说分为两类问题:

这就像你去理发店,希望 Tony 老师稍微剪短一点,结果却被剪得稍微短了点。而这需要我们在开始之前对齐认知,补充描述和参照。在这个前置阶段,即使发现对方理解有偏差,也还能及时纠正。俗称“对齐颗粒度”。

2.2 从规划到执行

Cursor 产出的代码由它所接收的上下文决定,如果没有准确描述需求意图,它会通过推断做出假设,导致产出不准确。因此我们在使用 Cursor 时,关键在于区分开发过程中的规划阶段执行阶段。在这个分层的视角下,不管是自身的关注点还是 AI 的角色定位都变得更加清晰:

Cursor 在这个过程中,不应该被视为开发者的替代品,而是一面能够放大开发者能力的镜子:

  • 对于已知的部分,Cursor 可以加速实现,减少重复劳动。

  • 对于未知的部分,Cursor 可以协助探索,但不能替代开发者的判断。

在理解了 AI 的角色后,我们需要重构目前的开发工作流,让 AI 成为真正有效的助手。最关键的转变是:**不再试图让 AI 替代开发流程中的任何环节,而是让它协助完成每个环节。**这意味着不是把 PRD 扔给 AI,等待完整代码,而是和 AI 一起理解 PRD 和代码现状,共同设计方案,明确步骤,然后分步实现。

三、现有问题

作为前端开发,我们的日常工作流程中大多围绕需求文档进行代码产出。这需要介于

  1. 我们对业务需求的理解。

  2. 对所属业务和项目现状的认知。

  3. 从而进行方案设计和决策,整理思路将复杂问题分解成可执行的粒度。

但同时,这导致我们不得不面临着一个矛盾:方案设计对效率的影响。一方面,方案设计是保证质量的必要环节;另一方面,生成和维护这些产物又会显著降低开发效率。尤其是在快速迭代的项目需求中,这种矛盾更为突出。

有时即使是一个小需求,可能也需要经过大量前置分析,才能进入开发。举个例子,以下是某个小需求的前端方案截图,通过不同的颜色区分了各流程的占比。从图中可以看出,各模块中绿色和蓝色所对应的「现状分析」和「改动方案」后占据了主要的篇幅,与相应的时间占用成正比。

图片

前端方案中的各环节分布

传统的解决方案通常是:

  • 模板化方案设计,减少重复工作。

  • 简化方案设计,减少不必要的细节描述。

  • 提高团队熟练度,使得方案设计生成更加高效。

作为附加项,现在我们能在这些基础上借助 Cursor 进一步提升效能。

四、协作流程

4.1 反馈循环

在协作时,关键在于对 Cursor 补充上下文,并对 Cursor 提供的结论进行人工核验,两者构成反馈循环。前者是希望 Cursor 知道,后者是需要我们自己知道,从而保障产出的结果符合预期。

图片

整体的 Cursor 协作流程分为规划和执行两个阶段。规划阶段专注于产出方案,执行阶段根据方案产出代码,两者交替执行。

4.2 流程对比

相较于以往,在使用 Cursor 后的工作模式差异如下:

图片

乍一看使用 Curosr 后流程更加繁琐,而实际上也确实如此。

所以这里更推荐换一个心态来看待流程上的变化,不必为了使用工具而使用。过去我们面向 Google / GitHub / Stack Overflow 编程也并不是因为我们为了搜索而搜索,是因为在具体开发中遇到了不明确的信息需要确认,现在这个角色可以渐进地由 Cursor 替代,比起搜索引擎,Cursor 能充分地根据项目现状分析出更贴切的答案,如同行车的导航和选购的得物,为此不必有太多的心理负担。

五、场景应用

重新回到在需求开发工作中的问题,占据我代码之外的主要工作是“现状分析”、“改动方案”和“影响评估”,因此主要分享这三个场景中的 Cursor 使用体验。

关于提示词,可根据实际需要使用 notepads 或 rules 降低单次使用成本。

5.1 现状分析

在需求开发过程中,我们时常会接触到陌生的业务模块,如何理解现状往往是最耗时也最容易被忽视的部分。如果对现状不够了解,当需求相对复杂或者项目本身存在较多的历史债务时,我们很难输出符合预期的方案,更难以保证最终代码的质量。对于新接手项目的开发者而言,这一阶段常常伴随着无数次的"代码考古"和"问询前人"。

Cursor 离代码上下文更近,我们可以在它的协助下抽丝剥茧,快速了解业务主线。这是一个学习的过程,当知道的越多,在后续的设计和开发中就越能正确地引导 Cursor。

具体可以从需求的目标改动点开始,梳理其所属功能和实现方式,包含交互流程、数据管理和条件渲染等:

业务需求
    ├── 1. 功能
    │   ├── 2. 实现 
    │   ... └── 3. 字段
    ...
目标 了解业务功能 了解代码实现 了解字段依赖
提示词参考 当前功能如何运作,用户交互有哪些路径,具体数据流向是怎样的,请整理成 mermaid 时序图。 当前代码如何组织,核心模块有哪些,组件间如何通信,梳理组件关系图。 梳理当前表单字段的显隐关系、联动逻辑以及数据源。
效果 输出所属功能中的角色和角色之间的交互方式,能快速掌握业务模块的大体脉络。 输出组件职责和组件间的关系,以便在投入开发前以组件模块维度确定改动范围。 能直观地呈现表单字段间的联动说明。

通过对上述三个层面的不断往复,Cursor 提供的直观输入能帮助我们摆脱掉一知半解的状态,消除不确定性也就消除了焦虑。

5.2 改动方案

在了解了现状后,开始面向需求进行改动方案设计。

在问答中,Cursor 倾向于直接满足表面的需求,但可能会忽略一些深层的系统设计考虑。当遇到复杂的问题时,建议先让 Cursor 分析问题本身,而不是直接要求它给出解决方案。通过引导它进行更全面的思考,能防止 Cursor 胡编乱造,确保它理解需求,同时也能暴露自身的思考局限,减少返工。具体做法可以先提示 “在我让你写代码之前不要生成代码” 以及 “先逐步分析需求再说明你打算怎么做”;

另一方面,由于 Cursor 背后 LLM 的 Context Window 存在上下文长度限制,意味着 Cursor 跟我们一样都存在“短期记忆”,这体现在当对话超出范围后,Cursor 会在输出方案和代码时,遗忘此前的要求和结论,造成不准确。因此,为了将短期记忆转换成长期记忆,需要我们对复杂任务进行必要的拆解,每次只专注于单个粒度下的问答,当确认方案后,再让 Cursor 汇总并记录到外置文档,以便在后续的对话中补充上下文(也可以借助 @Summarized Composers 实现)。在面对下一个任务时,开启新的会话进行问答,多轮下来形成由不同模块组装而成的方案设计。

这样一来,在生成代码阶段,Cursor 所需要面对的只是局部复杂度中的改动,这能很大程度上减缓我们在代码审核和验证上的投入成本。Cursor 也能始终保持在长度限制范围内,面对精炼后的方案设计进行决策和产出。

因此在整体流程上:

1. 拆解需求,缩小关注范围

2. 明确目标,清晰表达需求描述

  • Cursor 提供方案

  • 检查是否有理解偏差,并不断调整提示

  • 在确认方案后,最终由 Cursor 汇总成果

3. 渐进开发,分模块由 Cursor 生成代码,及时验证效果和审核代码

提示词参考:

  • 方案设计
我们先探讨方案,在我让你写代码之前不要生成代码
如果此处要加个 xxx 该怎么做,请先逐步分析需求
在想明白后向我说明为什么要这么设计
  • 代码产出,在功能之外,留意识别边界场景以及控制影响面
在写代码时遵循最小改动原则,避免影响原先的功能
即使识别到历史问题也不要自行优化,可以先告知我问题描述和对当前需求的影响,不要直接改跟本次需求无关的代码

5.3 影响评估

除去开发之前的方案耗时,在完成开发后,我们所要解决的是如何保障自测质量的问题。对于研发而言,需要关注的是在这个需求迭代内,改动点所关联的调用链路,而在这个路径依赖下不断冒泡所涉及到的具体功能就是影响面。

因此可以从两个方面提高自测可信度

  • 自下而上:基于改动代码和依赖项进行白盒测试,这需要研发自身投入必要的时间进行代码审核;

  • 自上而下:识别改动最终涉及到的页面和功能进行黑盒测试,逐个回归和确认功能是否符合预期。

图片

借助 Cursor 可以很低成本地分析改动,并按需产出测试用例,通过 @git 指令让 Cursor 参与到对当前功能分支或具体 commit 的评估:

图片

目标 代码审查 功能验证
提示词 @git逐个文件分析并总结改动点,评估是否引入了新的问题。 @git基于代码变更输出自测用例清单。
效果 在列举出每个文件的改动意图后,会告知潜在问题和修改意见。 围绕改动,生成新旧功能在不同场景中的测试用例。

六、小结

过去,成为一名优秀开发者需要经历漫长的积累:从反复查阅文档、在搜索引擎中筛选有效信息,到系统掌握编程语言、算法与网络原理,每一步都在构建扎实的「知识护城河」。而 AI 时代颠覆了这一逻辑 —— 当大模型能快速生成代码、解析技术方案时,开发者的核心能力似乎从“记忆与执行”转向成了“正确地提问,让 AI 提供答案”。

客观来看,AI 降低了信息获取的门槛,能更快地落地想法、验证思路。不变的是,好的答案源于好的问题,而提出好问题依旧需要积累专业领域下的知识,知道的越清楚才能在提问时描述得越清晰。

所有事都有吃力不讨好的部分,随着 Cursor 等 AI 工具在工程中的应用,我们可以逐渐将这部分职能分配出去,利用我们的知识储备,描述问题,引导过程,审核结果。工具的使用始终是为了节省人类体力和脑力的开销,从而在提升体验的同时提升生产力,以更充沛的精力聚焦在工作成果和个人成长上。

往期回顾

1.得物 iOS 启动优化之 Building Closure

2.分布式数据一致性场景与方案处理分析|得物技术

3.从对话到自主行动:AI应用如何从 Chat 进化为 Agent?开源项目源码深度揭秘|得物技术

4.得物技术部算法项目管理实践分享

5.商家域稳定性建设之原理探索|得物技术

文 / 魏命名

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

webpack 格式化模块工厂 第 三 节

作者 excel
2025年4月8日 07:59

cleanupForCache() :

  • 作用:清理缓存时调用的清理方法。遍历所有恢复的不安全缓存条目,执行相关的清理操作,包括清除模块的 Chunk 图、模块图,并执行模块的自清理操作。

// 清理缓存时需要调用的清理方法
cleanupForCache() {
// 遍历所有恢复的不安全缓存条目,执行相关清理操作
for (const module of this._restoredUnsafeCacheEntries) {
ChunkGraph.clearChunkGraphForModule(module); // 清除模块的 Chunk 图
ModuleGraph.clearModuleGraphForModule(module); // 清除模块的模块图
module.cleanupForCache(); // 执行模块自身的清理操作
}
}

create(data, callback) :

  • 作用:创建模块。通过处理依赖项,解析请求,并根据解析结果生成模块。它依赖 beforeResolvefactorize 钩子函数,执行相应的钩子操作,最终返回包含模块、文件依赖、缺失依赖等信息的结果。
/**
 * 创建模块的工厂方法
 * @param {ModuleFactoryCreateData} data 数据对象
 * @param {function((Error | null)=, ModuleFactoryResult=): void} callback 回调函数
 * @returns {void}
 */
create(data, callback) {
// 获取依赖项数组
const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
const context = data.context || this.context; // 上下文
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS; // 解析选项
const dependency = dependencies[0]; // 获取第一个依赖项
const request = dependency.request; // 依赖的请求
const assertions = dependency.assertions; // 依赖的断言
const dependencyType = dependency.category || ""; // 依赖的类型
const contextInfo = data.contextInfo; // 上下文信息
// 文件依赖、缺失依赖、上下文依赖集合
const fileDependencies = new LazySet();
const missingDependencies = new LazySet();
const contextDependencies = new LazySet();
/** @type {ResolveData} */
const resolveData = {
contextInfo,
resolveOptions,
context,
request,
assertions,
dependencies,
dependencyType,
fileDependencies,
missingDependencies,
contextDependencies,
createData: {},
cacheable: true // 缓存标志
};
// 调用 beforeResolve 钩子
this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {
if (err) {
// 如果出现错误,返回并且传递依赖项
return callback(err, {
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: false
});
}

// 如果结果为 false,表示忽略此模块
if (result === false) {
/** @type {ModuleFactoryResult} */
const factoryResult = {
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: resolveData.cacheable
};

if (resolveData.ignoredModule) {
factoryResult.module = resolveData.ignoredModule;
}

return callback(null, factoryResult);
}

// 如果结果为对象,则抛出错误
if (typeof result === "object")
throw new Error(
deprecationChangedHookMessage(
"beforeResolve",
this.hooks.beforeResolve
)
);

// 调用 factorize 钩子生成模块
this.hooks.factorize.callAsync(resolveData, (err, module) => {
if (err) {
// 如果出现错误,返回并且传递依赖项
return callback(err, {
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: false
});
}

/** @type {ModuleFactoryResult} */
const factoryResult = {
module,
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: resolveData.cacheable
};

callback(null, factoryResult);
});
});
}

resolveResource(contextInfo, context, unresolvedResource, resolver, resolveContext, callback) :

  • 作用:解析资源。使用给定的解析器 resolver 来解析资源路径。如果解析失败,调用 _resolveResourceErrorHints 方法生成错误提示,并在解析成功后返回解析结果。

/**
 * 解析资源方法
 * @param {ModuleFactoryCreateDataContextInfo} contextInfo 上下文信息
 * @param {string} context 上下文
 * @param {string} unresolvedResource 未解析的资源
 * @param {ResolverWithOptions} resolver 解析器
 * @param {ResolveContext} resolveContext 解析上下文
 * @param {(err: null | Error, res?: string | false, req?: ResolveRequest) => void} callback 回调函数
 */
resolveResource(
contextInfo,
context,
unresolvedResource,
resolver,
resolveContext,
callback
) {
// 调用解析器的 resolve 方法解析资源
resolver.resolve(
contextInfo,
context,
unresolvedResource,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
if (err) {
// 如果解析失败,调用 _resolveResourceErrorHints 生成错误提示
return this._resolveResourceErrorHints(
err,
contextInfo,
context,
unresolvedResource,
resolver,
resolveContext,
(err2, hints) => {
if (err2) {
err.message += `\n发生了解析附加提示时的致命错误:${err2.message}`;
err.stack += `\n发生了解析附加提示时的致命错误:\n${err2.stack}`;
return callback(err);
}
if (hints && hints.length > 0) {
err.message += `\n${hints.join("\n\n")}`;
}

// 检查扩展名是否缺少前导点(例如 "js" 而不是 ".js")
let appendResolveExtensionsHint = false;
const specifiedExtensions = Array.from(
resolver.options.extensions
);
const expectedExtensions = specifiedExtensions.map(extension => {
if (LEADING_DOT_EXTENSION_REGEX.test(extension)) {
appendResolveExtensionsHint = true;
return `.${extension}`;
}
return extension;
});
if (appendResolveExtensionsHint) {
err.message += `\n你是不是在 'resolve.extensions' 中遗漏了前导点?你是否打算将 '${JSON.stringify(
expectedExtensions
)}' 替换为 '${JSON.stringify(specifiedExtensions)}'?`;
}

callback(err);
}
);
}
// 返回解析的结果
callback(err, resolvedResource, resolvedResourceResolveData);
}
);
}

_resolveResourceErrorHints(error, contextInfo, context, unresolvedResource, resolver, resolveContext, callback) :

  • 作用:当解析失败时,生成详细的错误提示。它通过多个回调异步地提供错误解决的建议,比如缺少扩展名、错误的资源路径或强制扩展名的配置问题等。



/**
 * 解析资源错误时生成提示信息
 * @param {Error} error 错误信息
 * @param {ModuleFactoryCreateDataContextInfo} contextInfo 上下文信息
 * @param {string} context 上下文
 * @param {string} unresolvedResource 未解析的资源
 * @param {ResolverWithOptions} resolver 解析器
 * @param {ResolveContext} resolveContext 解析上下文
 * @param {Callback<string[]>} callback 回调函数
 * @private
 */
_resolveResourceErrorHints(
error,
contextInfo,
context,
unresolvedResource,
resolver,
resolveContext,
callback
) {
// 异步执行多个回调并获取错误提示
asyncLib.parallel(
[
callback => {
if (!resolver.options.fullySpecified) return callback();
// 在 fullySpecified 为 false 的情况下,尝试重新解析资源
resolver
.withOptions({
fullySpecified: false
})
.resolve(
contextInfo,
context,
unresolvedResource,
resolveContext,
(err, resolvedResource) => {
if (!err && resolvedResource) {
const resource = parseResource(resolvedResource).path.replace(
/^.*[\\/]/,
""
);
return callback(
null,
`你是否想要使用 '${resource}'?
突破性更改:请求 '${unresolvedResource}' 失败,仅仅因为它被解析为完全指定的(可能是因为来源是严格的 EcmaScript 模块,如具有 javascript 类型的模块,'*.mjs' 文件,或包含 '"type": "module"' 的 '*.js' 文件)。
此请求需要附加扩展名才能完全指定。`
);
}
callback();
}
);
},
callback => {
if (!resolver.options.enforceExtension) return callback();
// 在 enforceExtension 为 false 的情况下,尝试重新解析资源
resolver
.withOptions({
enforceExtension: false,
extensions: []
})
.resolve(
contextInfo,
context,
unresolvedResource,
resolveContext,
(err, resolvedResource) => {
if (!err && resolvedResource) {
let hint = "";
const match = /(\.[^.]+)(\?|$)/.exec(unresolvedResource);
if (match) {
const fixedRequest = unresolvedResource.replace(
/(\.[^.]+)(\?|$)/,
"$2"
);
hint = resolver.options.extensions.has(match[1])
? `你是否想使用 '${fixedRequest}'?`
: `你是否想使用 '${fixedRequest}'? 另请注意, '${match[1]}' 还未添加到 'resolve.extensions' 中,需将其添加进来以使其生效。`;
} else {
hint =
"你是否打算省略扩展名或移除 'resolve.enforceExtension' 设置?";
}
return callback(
null,
`请求 '${unresolvedResource}' 失败,仅仅因为 'resolve.enforceExtension' 被启用。
${hint}
现在无法在请求中包含扩展名。你是否打算在请求中强制包括扩展名,并使用 'resolve.extensions: []' 来强制执行此操作?`
);
}
callback();
}
);
},
callback => {
// 解析路径中的相对请求
if (
/^\.\.?\//.test(unresolvedResource) ||
resolver.options.preferRelative
) {
return callback();
}
resolver.resolve(
contextInfo,
context,
`./${unresolvedResource}`,
resolveContext,
(err, resolvedResource) => {
if (err || !resolvedResource) return callback();
const moduleDirectories = resolver.options.modules
.map(m => (Array.isArray(m) ? m.join(", ") : m))
.join(", ");
callback(
null,
`你是否想使用 './${unresolvedResource}'?
请求应该以 './' 开头,表示在当前目录解析。
如果不能更改源代码,你还可以使用 'preferRelative' 选项尝试在当前目录解析这些请求。`
);
}
);
}
],
(err, hints) => {
if (err) return callback(err);
callback(null, /** @type {string[]} */(hints).filter(Boolean));
}
);
}

getParser(type, parserOptions = EMPTY_PARSER_OPTIONS) :

  • 作用:获取解析器。根据类型 type 和解析器选项 parserOptions,从缓存中获取或创建一个新的解析器。通过缓存机制优化性能,避免重复创建解析器。



/**
 * 解析请求数组
 * @param {string} type 类型
 * @param {ParserOptions} parserOptions 解析器选项
 * @returns {Parser} 解析器
 */
getParser(type, parserOptions = EMPTY_PARSER_OPTIONS) {
let cache = this.parserCache.get(type);

if (cache === undefined) {
cache = new WeakMap();
this.parserCache.set(type, cache);
}

let parser = cache.get(parserOptions);

if (parser === undefined) {
parser = this.createParser(type, parserOptions);
cache.set(parserOptions, parser);
}

return parser;
}

webpack 格式化模块工厂 第 二 节

作者 excel
2025年4月8日 07:50

这段代码为 Webpack 或类似工具的模块加载系统的核心部分,提供了强大的扩展性和配置灵活性。通过钩子机制,它允许开发者在模块解析、构建、加载、生成等各个阶段插入自定义逻辑,以满足不同的构建需求。

1. 构造函数参数

  • context: 模块解析的上下文(如当前工作目录)。
  • fs: 用于文件操作的文件系统对象。
  • resolverFactory: 解析器工厂,用于创建各种解析器。
  • options: 配置选项,包括默认规则、规则集、解析器选项、生成器选项等。
  • associatedObjectForCache: 用于缓存相关操作的对象。
  • layers: 是否启用层级功能。

2. 钩子定义

  • resolve: 解析模块的钩子,处理模块解析的结果。
  • resolveForScheme, resolveInScheme: 钩子映射,用于根据不同的 Scheme(资源解析方案)处理解析数据。
  • factorize: 用于模块的因式分解,创建模块实例。
  • beforeResolve, afterResolve: 在模块解析之前和之后执行的钩子。
  • createModule: 根据解析数据创建模块的钩子。
  • module: 用于修改已创建模块的钩子。
  • createParser, parser: 解析器相关的钩子。
  • createGenerator, generator: 生成器相关的钩子。
  • createModuleClass: 根据解析数据创建模块类的钩子。

3. 主要功能

  • 解析模块:通过 resolve 钩子和 resolverFactory,对模块资源进行解析,支持多种方案(如不同的加载器、解析器等)。
  • 模块因式分解:通过 factorize 钩子,根据解析的数据创建模块实例。
  • 解析与加载器的关系:构造函数内部会根据加载器规则,处理不同类型的加载器(如预加载器、普通加载器、后加载器等)。
  • 规则集编译与应用:编译并应用规则集(包括默认规则和用户自定义规则),用于处理不同资源的加载方式。
  • 资源与依赖管理:管理文件依赖、缺失依赖、上下文依赖等,确保在模块解析过程中正确处理。

4. 复杂的资源解析流程

  • 解析过程中考虑了不同的资源类型、请求路径、上下文路径等,并支持通过各种钩子进行扩展。
  • 钩子的执行顺序、阶段设置(如 stage)允许在不同的生命周期阶段执行特定的操作。
  • 针对每种资源类型(如 JavaScript、CSS 等),系统通过不同的钩子和规则进行相应的处理。

5. 模块创建与修改

  • 在模块解析完成后,使用 createModule 钩子根据解析数据创建模块实例。
  • 通过 module 钩子,允许在模块创建后进一步修改其属性或行为。
  • 通过 createModuleClass 钩子,支持根据解析数据创建不同的模块类。

6. 缓存机制

  • 使用 parserCachegeneratorCache 缓存解析器和生成器,优化模块的处理性能。
  • 通过 associatedObjectForCache 对象,缓存解析和生成过程中的数据,避免重复计算。

7. 异步与回调管理

  • 代码中大量使用异步钩子(如 AsyncSeriesBailHook)和回调函数(如 callback),确保在模块解析和创建过程中能够处理异步操作,保证流程的顺利进行。

8. 错误处理与异常管理

  • 代码中通过多次检查并返回错误(如解析失败、未提供必需的参数等)确保异常的处理,防止出现不符合预期的行为。
/**
 * @param {object} param 参数
 * @param {string=} param.context 上下文
 * @param {InputFileSystem} param.fs 文件系统
 * @param {ResolverFactory} param.resolverFactory 解析器工厂
 * @param {ModuleOptions} param.options 模块选项
 * @param {object} param.associatedObjectForCache 缓存将附加到的对象
 * @param {boolean=} param.layers 启用层级
 */
constructor({
context,
fs,
resolverFactory,
options,
associatedObjectForCache,
layers = false
}) {
super(); // 调用父类构造函数,初始化基类
this.hooks = Object.freeze({
/** 
 * @type {AsyncSeriesBailHook<[ResolveData], Module | false | void>}
 * 用于解析模块的钩子。可以返回一个 Module 实例,或者返回 false 表示忽略该解析,或者返回 void。
 */
resolve: new AsyncSeriesBailHook(["resolveData"]),

/** 
 * @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} 
 * 钩子映射,针对不同的 scheme 解析资源数据。返回 true 或 void。
 */
resolveForScheme: new HookMap(
() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
),

/** 
 * @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} 
 * 与 `resolveForScheme` 相似,但用于特定的 scheme 上下文。
 */
resolveInScheme: new HookMap(
() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
),

/** 
 * @type {AsyncSeriesBailHook<[ResolveData], Module | undefined>} 
 * 用于模块的因式分解钩子。返回 Module 实例或 undefined。
 */
factorize: new AsyncSeriesBailHook(["resolveData"]),

/** 
 * @type {AsyncSeriesBailHook<[ResolveData], false | void>} 
 * 在解析模块之前调用的钩子。可以返回 false 以阻止解析过程。
 */
beforeResolve: new AsyncSeriesBailHook(["resolveData"]),

/** 
 * @type {AsyncSeriesBailHook<[ResolveData], false | void>} 
 * 在解析模块之后调用的钩子。可以返回 false 以停止进一步的处理。
 */
afterResolve: new AsyncSeriesBailHook(["resolveData"]),

/** 
 * @type {AsyncSeriesBailHook<[ResolveData["createData"], ResolveData], Module | void>} 
 * 用于根据解析数据创建模块的钩子。
 */
createModule: new AsyncSeriesBailHook(["createData", "resolveData"]),

/** 
 * @type {SyncWaterfallHook<[Module, ResolveData["createData"], ResolveData]>} 
 * 用于修改已创建模块的钩子,模块已解析并传递了工厂数据。
 */
module: new SyncWaterfallHook(["module", "createData", "resolveData"]),

/** 
 * @type {HookMap<SyncBailHook<[ParserOptions], Parser | void>>} 
 * 钩子映射,用于创建解析器。如果解析器创建失败,则返回 void。
 */
createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),

/** 
 * @type {HookMap<SyncBailHook<[TODO, ParserOptions], void>>} 
 * 用于修改解析器的钩子,解析器创建之后执行。
 */
parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),

/** 
 * @type {HookMap<SyncBailHook<[GeneratorOptions], Generator | void>>} 
 * 钩子映射,用于创建生成器。返回生成器实例或 void 表示没有生成。
 */
createGenerator: new HookMap(
() => new SyncBailHook(["generatorOptions"])
),

/** 
 * @type {HookMap<SyncBailHook<[TODO, GeneratorOptions], void>>} 
 * 用于修改生成器的钩子,生成器创建之后执行。
 */
generator: new HookMap(
() => new SyncHook(["generator", "generatorOptions"])
),

/** 
 * @type {HookMap<SyncBailHook<[TODO, ResolveData], Module | void>>} 
 * 钩子映射,用于根据解析数据创建模块类。
 */
createModuleClass: new HookMap(
() => new SyncBailHook(["createData", "resolveData"])
)
});

this.resolverFactory = resolverFactory; // 初始化模块解析器工厂
this.ruleSet = ruleSetCompiler.compile([  // 编译规则集,包括默认规则和用户定义的规则
{
rules: options.defaultRules
},
{
rules: options.rules
}
]);
this.context = context || ""; // 设置模块解析的上下文(目录)
this.fs = fs; // 设置文件系统,用于文件解析
this._globalParserOptions = options.parser; // 存储全局的解析器选项
this._globalGeneratorOptions = options.generator; // 存储全局的生成器选项

/** @type {Map<string, WeakMap<object, Parser>>} */
this.parserCache = new Map(); // 缓存不同类型资源的解析器
/** @type {Map<string, WeakMap<object, Generator>>} */
this.generatorCache = new Map(); // 缓存不同类型资源的生成器
/** @type {Set<Module>} */
this._restoredUnsafeCacheEntries = new Set(); // 存储已恢复的模块

// 绑定并缓存解析资源
const cacheParseResource = parseResource.bindCache(
associatedObjectForCache
);
const cachedParseResourceWithoutFragment =
parseResourceWithoutFragment.bindCache(associatedObjectForCache);
this._parseResourceWithoutFragment = cachedParseResourceWithoutFragment;

// 因式分解钩子,通过解析数据创建模块
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",  // 为此钩子命名,便于识别
stage: 100  // 设置钩子链中的执行阶段
},
(resolveData, callback) => {
// 首先通过 `resolve` 钩子解析数据
this.hooks.resolve.callAsync(resolveData, (err, result) => {
if (err) return callback(err);  // 处理解析时的错误

// 如果返回值是 false,表示忽略该解析
if (result === false) return callback();

// 如果返回值是 Module 实例,立即返回该模块
if (result instanceof Module) return callback(null, result);

// 如果返回的是对象(但不是 Module),则抛出弃用的错误
if (typeof result === "object")
throw new Error(
`${deprecationChangedHookMessage(
"resolve",
this.hooks.resolve
)} Returning a Module object will result in this module used as result.`
);

// 调用 `afterResolve` 钩子,在解析后执行
this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
if (err) return callback(err);  // 处理 afterResolve 钩子中的错误

// 如果返回值是对象,表示弃用,抛出错误
if (typeof result === "object")
throw new Error(
deprecationChangedHookMessage(
"afterResolve",
this.hooks.afterResolve
)
);

// 如果返回值是 false,表示忽略该解析
if (result === false) return callback();

const createData = resolveData.createData;

// 使用 `createModule` 钩子创建模块
this.hooks.createModule.callAsync(
createData,
resolveData,
(err, createdModule) => {
if (!createdModule) {
if (!resolveData.request) {
return callback(new Error("Empty dependency (no request)"));
}

// 如果未创建模块,使用模块类创建
createdModule = this.hooks.createModuleClass
.for(
/** @type {ModuleSettings} */
(createData.settings).type
)
.call(createData, resolveData);

// 如果没有模块类,则默认使用 NormalModule 创建模块
if (!createdModule) {
createdModule = /** @type {Module} */ (
new NormalModule(
/** @type {NormalModuleCreateData} */
(createData)
)
);
}
}

// 应用 `module` 钩子,修改模块后再返回
createdModule = this.hooks.module.call(
createdModule,
createData,
resolveData
);

// 最后返回创建的模块
return callback(null, createdModule);
}
);
});
});
}
);
// 注册 resolve 钩子,处理模块资源解析
this.hooks.resolve.tapAsync(
{
name: "NormalModuleFactory", // 钩子的名称
stage: 100 // 钩子的执行阶段,值越小,越早执行
},
(data, callback) => {
const {
contextInfo,
context,
dependencies,
dependencyType,
request,
assertions,
resolveOptions,
fileDependencies,
missingDependencies,
contextDependencies
} = data; // 解构获取传入的数据

const loaderResolver = this.getResolver("loader"); // 获取 loader 解析器

let matchResourceData; // 匹配的资源数据
let unresolvedResource; // 未解析的资源
let elements; // loader 元素
let noPreAutoLoaders = false; // 是否没有预加载器
let noAutoLoaders = false; // 是否没有加载器
let noPrePostAutoLoaders = false; // 是否没有预后加载器

const contextScheme = getScheme(context); // 获取上下文的 Scheme
let scheme = getScheme(request); // 获取请求的 Scheme

if (!scheme) {
let requestWithoutMatchResource = request;
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request); // 匹配资源

if (matchResourceMatch) {
let matchResource = matchResourceMatch[1];
if (matchResource.charCodeAt(0) === 46) { // 检查是否以 . 或 ../ 开头
const secondChar = matchResource.charCodeAt(1);
if (secondChar === 47 || (secondChar === 46 && matchResource.charCodeAt(2) === 47)) {
matchResource = join(this.fs, context, matchResource); // 处理相对路径
}
}
matchResourceData = {
resource: matchResource,
...cacheParseResource(matchResource)
};
requestWithoutMatchResource = request.slice(matchResourceMatch[0].length); // 去除匹配到的资源部分
}

scheme = getScheme(requestWithoutMatchResource);

if (!scheme && !contextScheme) {
const firstChar = requestWithoutMatchResource.charCodeAt(0);
const secondChar = requestWithoutMatchResource.charCodeAt(1);
noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!"
const rawElements = requestWithoutMatchResource.slice(
noPreAutoLoaders || noPrePostAutoLoaders ? 2 : noAutoLoaders ? 1 : 0
).split(/!+/);
unresolvedResource = rawElements.pop(); // 获取未解析的资源
elements = rawElements.map(el => {
const { path, query } = cachedParseResourceWithoutFragment(el); // 解析 loader 的路径和查询字符串
return {
loader: path,
options: query ? query.slice(1) : undefined
};
});
scheme = getScheme(unresolvedResource);
} else {
unresolvedResource = requestWithoutMatchResource;
elements = EMPTY_ELEMENTS;
}
} else {
unresolvedResource = request;
elements = EMPTY_ELEMENTS;
}

const resolveContext = {
fileDependencies,
missingDependencies,
contextDependencies
};

let resourceData;

let loaders;

const continueCallback = needCalls(2, err => {
if (err) return callback(err);

try {
// 处理 loader 的配置
for (const item of loaders) {
if (typeof item.options === "string" && item.options[0] === "?") {
const ident = item.options.slice(1);
if (ident === "[[missing ident]]") {
throw new Error(
"No ident is provided by referenced loader. When using a function for Rule.use in config you need to provide an 'ident' property for referenced loader options."
);
}
item.options = this.ruleSet.references.get(ident);
if (item.options === undefined) {
throw new Error("Invalid ident is provided by referenced loader");
}
item.ident = ident;
}
}
} catch (identErr) {
return callback(identErr);
}

if (!resourceData) {
return callback(null, dependencies[0].createIgnoredModule(context)); // 如果没有资源数据,返回忽略模块
}

const userRequest = (matchResourceData !== undefined ? `${matchResourceData.resource}!=!` : "") +
stringifyLoadersAndResource(loaders, resourceData.resource);

const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];

let resource;
let match;

if (matchResourceData && typeof (resource = matchResourceData.resource) === "string" && (match = /\.webpack\[([^\]]+)\]$/.exec(resource))) {
settings.type = match[1];
matchResourceData.resource = matchResourceData.resource.slice(0, -settings.type.length - 10);
} else {
settings.type = JAVASCRIPT_MODULE_TYPE_AUTO;
const resourceDataForRules = matchResourceData || resourceData;
const result = this.ruleSet.exec({
resource: resourceDataForRules.path,
realResource: resourceData.path,
resourceQuery: resourceDataForRules.query,
resourceFragment: resourceDataForRules.fragment,
scheme,
assertions,
mimetype: matchResourceData ? "" : resourceData.data.mimetype || "",
dependency: dependencyType,
descriptionData: matchResourceData ? undefined : resourceData.data.descriptionFileData,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
issuerLayer: contextInfo.issuerLayer || ""
});

for (const r of result) {
if (r.type === "use") {
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (r.type === "use-post") {
if (!noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
}
} else if (r.type === "use-pre") {
if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
useLoadersPre.push(r.value);
}
} else if (typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
}

let postLoaders;
let normalLoaders;
let preLoaders;

const continueCallback = needCalls(3, err => {
if (err) return callback(err);

const allLoaders = postLoaders;
if (matchResourceData === undefined) {
loaders.forEach(loader => allLoaders.push(loader));
normalLoaders.forEach(loader => allLoaders.push(loader));
} else {
normalLoaders.forEach(loader => allLoaders.push(loader));
loaders.forEach(loader => allLoaders.push(loader));
}
preLoaders.forEach(loader => allLoaders.push(loader));

const type = settings.type;
const resolveOptions = settings.resolve;
const layer = settings.layer;

if (layer !== undefined && !layers) {
return callback(new Error("'Rule.layer' is only allowed when 'experiments.layers' is enabled"));
}

try {
Object.assign(data.createData, {
layer: layer === undefined ? contextInfo.issuerLayer || null : layer,
request: stringifyLoadersAndResource(allLoaders, resourceData.resource),
userRequest,
rawRequest: request,
loaders: allLoaders,
resource: resourceData.resource,
context: resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData ? matchResourceData.resource : undefined,
resourceResolveData: resourceData.data,
settings,
type,
parser: this.getParser(type, settings.parser),
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
});
} catch (createDataErr) {
return callback(createDataErr);
}
callback();
});

this.resolveRequestArray(contextInfo, this.context, useLoadersPost, loaderResolver, resolveContext, (err, result) => {
postLoaders = result;
continueCallback(err);
});

this.resolveRequestArray(contextInfo, this.context, useLoaders, loaderResolver, resolveContext, (err, result) => {
normalLoaders = result;
continueCallback(err);
});

this.resolveRequestArray(contextInfo, this.context, useLoadersPre, loaderResolver, resolveContext, (err, result) => {
preLoaders = result;
continueCallback(err);
});
});

this.resolveRequestArray(contextInfo, contextScheme ? this.context : context, elements, loaderResolver, resolveContext, (err, result) => {
if (err) return continueCallback(err);
loaders = result;
continueCallback();
});

const defaultResolve = context => {
if (/^($|\?)/.test(unresolvedResource)) {
resourceData = {
resource: unresolvedResource,
data: {},
...cacheParseResource(unresolvedResource)
};
continueCallback();
} else {
const normalResolver = this.getResolver("normal", dependencyType ? cachedSetProperty(resolveOptions || EMPTY_RESOLVE_OPTIONS, "dependencyType", dependencyType) : resolveOptions);
this.resolveResource(
contextInfo,
context,
unresolvedResource,
normalResolver,
resolveContext,
(err, _resolvedResource, resolvedResourceResolveData) => {
if (err) return continueCallback(err);
if (_resolvedResource !== false) {
const resolvedResource = _resolvedResource;
resourceData = {
resource: resolvedResource,
data: resolvedResourceResolveData,
...cacheParseResource(resolvedResource)
};
}
continueCallback();
}
);
}
};

if (scheme) {
resourceData = {
resource: unresolvedResource,
data: {},
path: undefined,
query: undefined,
fragment: undefined,
context: undefined
};
this.hooks.resolveForScheme.for(scheme).callAsync(resourceData, data, err => {
if (err) return continueCallback(err);
continueCallback();
});
} else if (contextScheme) {
resourceData = {
resource: unresolvedResource,
data: {},
path: undefined,
query: undefined,
fragment: undefined,
context: undefined
};
this.hooks.resolveInScheme.for(contextScheme).callAsync(resourceData, data, (err, handled) => {
if (err) return continueCallback(err);
if (!handled) return defaultResolve(this.context);
continueCallback();
});
} else defaultResolve(context);
}
);

}

Lodash源码阅读-baseProperty

作者 好_快
2025年4月8日 05:30

Lodash 源码阅读-baseProperty

概述

baseProperty 是 Lodash 内部的工具函数,用于创建一个函数,这个函数接收一个对象并返回该对象上指定属性的值。它是 _.property 方法的核心实现之一,主要处理单层属性访问的场景。

前置学习

依赖函数

由于 baseProperty 实现简单,它没有直接依赖其他函数。但在 Lodash 的整体架构中,它与以下函数相关:

  • property:外部接口,根据路径类型选择使用 basePropertybasePropertyDeep
  • isKey:判断路径是否为单一键值
  • toKey:将路径值转换为有效的属性键
  • basePropertyDeep:处理深层属性路径访问的函数

技术知识

  • 高阶函数:返回函数的函数
  • 闭包:函数捕获外部变量
  • 空值处理:安全地处理 null 和 undefined
  • 函数柯里化:将多参数函数转换为单参数函数序列

源码实现

function baseProperty(key) {
  return function (object) {
    return object == null ? undefined : object[key];
  };
}

实现思路

baseProperty 的实现非常简洁明了:

  1. 接收一个属性键名 key 作为参数
  2. 返回一个新函数,这个函数接收一个对象 object 作为参数
  3. 在返回的函数内部,先检查对象是否为 null 或 undefined
  4. 如果对象为空值,返回 undefined;否则,返回对象的指定属性值

这种设计采用了函数式编程的柯里化思想,将"获取对象属性"拆分为两步:先确定要访问的属性,然后在需要时提供对象并获取值。

源码解析

函数签名

function baseProperty(key) {
  // ...
}

函数接收一个参数 key,表示要访问的属性键名,通常是字符串类型。

返回高阶函数

return function (object) {
  return object == null ? undefined : object[key];
};

这里有几个关键点:

  1. 返回新函数:创建并返回一个接收 object 参数的新函数,实现了高阶函数模式

  2. 空值检查:使用 object == null 检查对象是否为空值

    • 使用松散等于操作符 == 同时处理 nullundefined
    • 这是防御性编程的体现,避免在访问空值的属性时抛出错误
  3. 属性访问:使用方括号语法 object[key] 获取属性值

    • 相比点语法,方括号语法允许使用变量作为属性名
    • 方括号语法更灵活,可以处理包含特殊字符的属性名
  4. 闭包应用:返回的函数捕获并"记住"了 key 参数

    • 这使得可以在不同时间、对不同对象重复使用同一个属性访问器

示例说明

// 创建一个获取对象 name 属性的函数
const getName = baseProperty("name");

// 使用这个函数获取对象的 name 属性
const user = { name: "Alice", age: 30 };
getName(user); // 返回 'Alice'

// 处理空值
getName(null); // 返回 undefined
getName(undefined); // 返回 undefined

// 处理不存在的属性
getName({ age: 25 }); // 返回 undefined

与 basePropertyDeep 的比较

basePropertybasePropertyDeep 构成了属性访问的两种实现:

// 处理单层属性访问
function baseProperty(key) {
  return function (object) {
    return object == null ? undefined : object[key];
  };
}

// 处理深层属性路径访问
function basePropertyDeep(path) {
  return function (object) {
    return baseGet(object, path);
  };
}

区别在于:

  • baseProperty 只能访问对象的直接属性,如 object.name
  • basePropertyDeep 可以访问嵌套对象的深层属性,如 object.user.profile.name
  • baseProperty 性能更好,因为它避免了路径解析的开销

property 函数中,根据路径类型选择使用哪个实现:

function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}

总结

baseProperty 是 Lodash 中一个简洁而实用的工具函数,它体现了以下特点:

  1. 简单高效:实现极其简洁,只关注单一职责——创建单层属性访问器

  2. 防御性编程:安全处理空值情况,避免运行时错误

  3. 函数式设计:利用高阶函数和闭包,支持函数组合和链式调用

  4. 性能优化:在 property 方法中作为处理简单属性访问的优化路径

baseProperty 的应用场景包括:

  • 从对象集合中提取特定属性值
  • 在排序、过滤等操作中作为访问器函数
  • 在函数式编程中作为数据转换的一部分

虽然代码量很小,但 baseProperty 在 Lodash 的函数式工具链中扮演着重要角色,是理解 Lodash 设计哲学的良好示例。

Lodash源码阅读-basePropertyDeep

作者 好_快
2025年4月8日 05:30

Lodash 源码阅读-basePropertyDeep

概述

basePropertyDeep 是 Lodash 内部的工具函数,用于创建属性访问器。它能生成一个函数,该函数接收对象并返回该对象在指定深层路径上的值。该函数与 baseProperty 互补,一起支持 Lodash 中 _.property 方法的完整功能。

前置学习

依赖函数

  • baseGet:核心函数,根据路径从对象中获取深层嵌套的属性值
  • castPath:将各种格式的路径(字符串、数组等)转换为统一的数组格式
  • toKey:将路径片段转换为有效的对象属性键

技术知识

  • 高阶函数:返回函数的函数
  • 闭包:函数能够记住并访问其定义时所在的词法作用域
  • 柯里化:将多参数函数转换为一系列单参数函数
  • 属性路径访问:以字符串或数组形式表示的嵌套对象访问路径

源码实现

function basePropertyDeep(path) {
  return function (object) {
    return baseGet(object, path);
  };
}

实现思路

basePropertyDeep 的实现思路非常简洁:

  1. 接收一个属性路径作为参数
  2. 返回一个新函数,该函数接收一个对象作为参数
  3. 在返回的函数内部,调用 baseGet 根据路径从对象中获取值

这种设计体现了函数式编程中的柯里化思想:先固定属性路径参数,创建一个可重用的访问器函数,然后在需要时传入不同的对象获取属性值。

源码解析

函数签名

function basePropertyDeep(path) {
  // ...
}

函数接收一个 path 参数,表示要访问的属性路径。路径可以是字符串形式(如 'a.b.c')或数组形式(如 ['a', 'b', 'c'])。

返回函数

return function (object) {
  return baseGet(object, path);
};

函数返回一个新的函数,这个新函数:

  • 接收一个 object 参数,表示要访问其属性的对象
  • 利用闭包特性"记住"了外部函数中的 path 参数
  • 调用 baseGet(object, path) 来获取对象在指定路径上的值

baseGet 函数

basePropertyDeep 本质上是对 baseGet 函数的封装:

function baseGet(object, path) {
  path = castPath(path, object);

  var index = 0,
    length = path.length;

  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  return index && index == length ? object : undefined;
}

baseGet 处理实际的属性访问逻辑:

  • 将路径标准化为数组格式
  • 逐层访问对象的嵌套属性
  • 安全处理 nullundefined
  • 返回找到的属性值或 undefined

与 baseProperty 对比

basePropertyDeepbaseProperty 是一对互补函数:

function baseProperty(key) {
  return function (object) {
    return object == null ? undefined : object[key];
  };
}

主要区别:

  • baseProperty 处理单层属性访问,直接使用 object[key] 语法,性能更好
  • basePropertyDeep 处理多层嵌套属性访问,使用 baseGet 进行复杂路径解析

应用场景

basePropertyDeep 在 Lodash 中主要通过 _.property 方法被使用:

// _.property 的简化实现
function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}

总结

basePropertyDeep 是一个设计精巧的函数,它通过简洁的实现提供了强大的深层属性访问能力。这个函数体现了 Lodash 的几个重要设计原则:

  1. 单一职责原则:专注于创建属性访问器,将实际的属性访问逻辑委托给 baseGet
  2. 函数式设计:利用高阶函数和闭包,支持函数复用和组合
  3. 一致的接口:与 baseProperty 保持相同的函数签名,便于在 _.property 中无缝集成
  4. 优雅的抽象:将复杂的深层属性访问封装成简单易用的函数

通过这种设计,Lodash 提供了灵活而强大的属性访问功能,使开发者能够简洁地处理复杂数据结构,同时保持代码的可读性和可维护性。

Lodash源码阅读-baseMatchesProperty

作者 好_快
2025年4月8日 05:30

Lodash 源码阅读-baseMatchesProperty

概述

baseMatchesProperty 是 Lodash 内部的工具函数,用来创建一个检查函数,这个函数会判断对象指定路径上的值是否与给定的源值相等。它是 _.matchesProperty 方法的核心实现,常用于集合操作中创建属性匹配断言函数。

前置学习

依赖函数

  • isKey:检查值是否为简单属性路径
  • isStrictComparable:检查值是否可以使用严格相等(===)比较
  • matchesStrictComparable:创建使用严格相等比较的匹配函数
  • toKey:将值转换为属性路径的键
  • get:获取对象指定路径上的值
  • hasIn:检查路径是否存在于对象中
  • baseIsEqual:深度比较两个值是否相等

技术知识

  • 高阶函数:返回函数的函数
  • 属性路径:表示嵌套对象属性的访问路径
  • 闭包:函数记住并访问其词法作用域
  • 位掩码:使用位运算符组合多个标志
  • 性能优化:通过快速路径提高函数执行效率

源码实现

function baseMatchesProperty(path, srcValue) {
  if (isKey(path) && isStrictComparable(srcValue)) {
    return matchesStrictComparable(toKey(path), srcValue);
  }
  return function (object) {
    var objValue = get(object, path);
    return objValue === undefined && objValue === srcValue
      ? hasIn(object, path)
      : baseIsEqual(
          srcValue,
          objValue,
          COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG
        );
  };
}

实现思路

baseMatchesProperty 根据路径和源值的特性,采用不同策略创建匹配函数:

  1. 当路径是简单属性键且源值可以使用严格相等比较时,采用简单高效的 matchesStrictComparable 函数

  2. 否则,创建一个更复杂的匹配函数,这个函数会:

    • 获取对象指定路径上的值
    • 处理特殊情况(如 undefined 值)
    • 使用深度比较判断属性值是否匹配

这种分层设计既保证了比较的准确性,又通过快速路径优化了简单场景的性能。

源码解析

快速路径优化

if (isKey(path) && isStrictComparable(srcValue)) {
  return matchesStrictComparable(toKey(path), srcValue);
}

这段代码是性能优化,当满足两个条件时使用简单高效的比较方式:

  1. path 是简单属性键(通过 isKey 判断)

    简单属性键是像 'name' 这样的直接属性,而不是 'user.profile.name'['user', 'profile', 'name'] 这样的嵌套路径

  2. srcValue 可以使用严格相等比较(通过 isStrictComparable 判断)

    可严格比较的值包括基本类型(字符串、数字、布尔值等)和非对象值

以下是 isStrictComparable 的实现:

function isStrictComparable(value) {
  return value === value && !isObject(value);
}

这个函数检查值是否可以使用 === 操作符安全比较,它排除了 NaN(因为 NaN !== NaN)和对象(需要深度比较)。

当条件满足时,matchesStrictComparable 会创建一个简单函数:

function matchesStrictComparable(key, srcValue) {
  return function (object) {
    if (object == null) {
      return false;
    }
    return (
      object[key] === srcValue &&
      (srcValue !== undefined || key in Object(object))
    );
  };
}

这个函数直接使用 === 比较,避免了更复杂的深度比较操作。

创建复杂匹配函数

return function (object) {
  var objValue = get(object, path);
  return objValue === undefined && objValue === srcValue
    ? hasIn(object, path)
    : baseIsEqual(
        srcValue,
        objValue,
        COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG
      );
};

当不满足快速路径条件时,函数返回一个更复杂的匹配函数:

  1. 使用 get(object, path) 获取对象指定路径上的值

    function get(object, path, defaultValue) {
      var result = object == null ? undefined : baseGet(object, path);
      return result === undefined ? defaultValue : result;
    }
    

    这支持深层嵌套路径访问,如 'user.profile.name'['user', 'profile', 'name']

  2. 处理特殊情况:对象值和源值都是 undefined

    objValue === undefined && objValue === srcValue;
    

    这种情况下,需要区分"路径不存在"和"路径存在但值为 undefined"

    通过 hasIn(object, path) 检查路径是否存在于对象中:

    function hasIn(object, path) {
      return object != null && hasPath(object, path, baseHasIn);
    }
    
  3. 对于其他情况,使用 baseIsEqual 进行深度比较

    baseIsEqual(
      srcValue,
      objValue,
      COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG
    );
    

    使用的位掩码标志:

    • COMPARE_PARTIAL_FLAG(值为 1):启用部分比较模式
    • COMPARE_UNORDERED_FLAG(值为 2):启用无序比较模式

    这允许比较复杂对象和数组,即使它们的属性顺序不同

实际案例:

// 创建一个检查函数,判断对象的 'user.type' 是否为 'admin'
const isAdmin = baseMatchesProperty("user.type", "admin");

// 使用该函数检查对象
isAdmin({ user: { type: "admin", name: "John" } }); // 返回 true
isAdmin({ user: { type: "user", name: "Jane" } }); // 返回 false

对于数组路径和复杂对象比较:

// 检查对象的 user.hobbies 数组是否包含 ['reading', 'coding']
const hasHobbies = baseMatchesProperty(
  ["user", "hobbies"],
  ["reading", "coding"]
);

// 由于使用 COMPARE_UNORDERED_FLAG,数组元素顺序不影响结果
hasHobbies({ user: { hobbies: ["coding", "reading"] } }); // 返回 true

总结

baseMatchesProperty 是 Lodash 中属性值匹配功能的核心实现,它有几个显著特点:

  1. 分层设计:根据输入参数特性选择不同实现策略,简单情况用快速方法,复杂情况用完整比较

  2. 性能优化:通过快速路径处理常见简单情况,减少不必要的复杂比较

  3. 完备性:处理了各种边缘情况,如 undefined 值和不存在的路径

  4. 灵活性:支持简单属性和复杂嵌套路径,同时支持基本值比较和深度对象比较

baseMatchesProperty 的典型应用场景包括:

  • 在集合操作(如 _.find_.filter)中作为断言函数
  • 实现数据查询和过滤条件
  • 创建基于属性值的数据验证器
  • 在函数式编程中组合构建复杂条件

这种实现方式体现了函数式编程的强大之处:高阶函数和闭包的结合使我们可以动态生成专用的比较函数,既保证了灵活性又维持了高性能。

Lodash源码阅读-hasIn

作者 好_快
2025年4月8日 05:29

Lodash 源码阅读-hasIn

概述

hasIn 是 Lodash 中的一个函数,用于检查对象中是否存在指定的属性路径,它不仅检查对象自身的属性,还会检查从原型链继承的属性。这个函数特别适合处理继承关系和需要考虑原型链的场景。

前置学习

依赖函数

  • hasPath:内部函数,是路径检查的核心实现,负责解析路径并逐级遍历检查
  • baseHasIn:内部函数,使用 in 操作符检查属性(包括继承属性)
  • castPath:将不同格式的路径转换为标准数组格式
  • toKey:将路径片段转换为有效的对象属性键

技术知识

  • JavaScript 原型链:JavaScript 中对象通过原型链继承属性和方法的机制
  • in 操作符:用于检查属性是否存在于对象或其原型链中
  • 属性路径:表示嵌套对象属性的方式,如 a.b.c['a', 'b', 'c']

源码实现

/**
 * Checks if `path` is a direct or inherited property of `object`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Object
 * @param {Object} object The object to query.
 * @param {Array|string} path The path to check.
 * @returns {boolean} Returns `true` if `path` exists, else `false`.
 * @example
 *
 * var object = _.create({ 'a': _.create({ 'b': 2 }) });
 *
 * _.hasIn(object, 'a');
 * // => true
 *
 * _.hasIn(object, 'a.b');
 * // => true
 *
 * _.hasIn(object, ['a', 'b']);
 * // => true
 *
 * _.hasIn(object, 'b');
 * // => false
 */
function hasIn(object, path) {
  return object != null && hasPath(object, path, baseHasIn);
}

实现思路

hasIn 函数的实现非常简洁,主要由以下几个关键步骤组成:

  1. 空值检查:确保输入对象不是 nullundefined
  2. 委托路径检查:将实际的路径检查工作委托给 hasPath 函数
  3. 使用 baseHasIn:通过传入 baseHasIn 函数作为检查方法,实现对继承属性的支持

这种设计让 hasIn 能够处理各种格式的路径输入,同时正确处理继承属性。

源码解析

函数解析

function hasIn(object, path) {
  return object != null && hasPath(object, path, baseHasIn);
}

这个简短的实现包含两个关键部分:

  1. object != null:检查对象是否存在(不是 null 和不是 undefined)。如果对象不存在,立即返回 false,避免后续操作出错。

  2. hasPath(object, path, baseHasIn):如果对象存在,调用 hasPath 函数进行路径检查:

    • object:要检查的对象
    • path:属性路径,可以是字符串(如 'a.b.c')或数组(如 ['a', 'b', 'c']
    • baseHasIn:用于检查单个属性的函数

baseHasIn 函数实现

function baseHasIn(object, key) {
  return object != null && key in Object(object);
}

baseHasIn 函数使用 in 操作符检查属性是否存在,这与 baseHas(使用 hasOwnProperty)不同,in 操作符会检查对象自身和原型链上的属性。

has 与 hasIn 的比较

为了更好地理解 hasIn,我们可以与 has 函数进行比较:

// has 实现
function has(object, path) {
  return object != null && hasPath(object, path, baseHas);
}

// hasIn 实现
function hasIn(object, path) {
  return object != null && hasPath(object, path, baseHasIn);
}

主要区别在于:

  • has 使用 baseHas,通过 hasOwnProperty 只检查对象自身的属性
  • hasIn 使用 baseHasIn,通过 in 操作符检查对象自身和原型链上的属性

总结

hasIn 是一个简洁而强大的函数,通过利用 JavaScript 的原型继承机制,它提供了一种检查对象属性(包括继承属性)的可靠方式。与只检查对象自身属性的 has 相比,hasIn 更适合处理继承场景、接口检测和框架开发。

其核心实现依赖于 hasPath 函数的路径遍历能力,以及 baseHasIn 函数通过 in 操作符检查属性的能力。这种组合让 hasIn 能够处理各种复杂的属性路径,同时正确识别继承的属性。

在实际应用中,hasIn 特别适合以下场景:

  1. 需要考虑继承属性的属性检查
  2. 检查对象是否实现了特定接口
  3. 处理原型链上的深层嵌套属性
  4. 安全地访问可能是继承的方法和属性

Lodash源码阅读-hasPath

作者 好_快
2025年4月8日 05:29

Lodash 源码阅读-hasPath

概述

hasPath 是 Lodash 的内部工具函数,用于检查对象中是否存在指定路径。它是 _.has_.hasIn 这两个公共 API 的核心实现,能够处理各种格式的路径查询并妥善处理各种边界情况。

前置学习

依赖函数

  • castPath:将各种格式的路径转换成标准数组格式,例如 "a.b.c" 转为 ["a", "b", "c"]
  • toKey:将路径中的各个部分转换为有效的属性键,特别处理了负零(-0)的情况
  • isLength:判断一个值是否是合法的数组长度(非负整数且小于等于最大安全整数)
  • isIndex:判断一个值是否是合法的数组索引(非负整数且小于给定的长度)
  • isArray:检查一个值是否为数组
  • isArguments:检查一个值是否为函数的 arguments 对象

技术知识

  • 属性路径:JavaScript 中表示嵌套对象路径的方式,如 user.profile.nameuser['profile']['name']
  • 对象属性访问:JavaScript 中通过点号或方括号访问对象属性的方法
  • 数组索引和稀疏数组:数组索引的特性及稀疏数组(含"空位"的数组)的概念
  • 类数组对象:具有数字索引和 length 属性但不是真正数组的对象

源码实现

function hasPath(object, path, hasFunc) {
  path = castPath(path, object);

  var index = -1,
    length = path.length,
    result = false;

  while (++index < length) {
    var key = toKey(path[index]);
    if (!(result = object != null && hasFunc(object, key))) {
      break;
    }
    object = object[key];
  }
  if (result || ++index != length) {
    return result;
  }
  length = object == null ? 0 : object.length;
  return (
    !!length &&
    isLength(length) &&
    isIndex(key, length) &&
    (isArray(object) || isArguments(object))
  );
}

实现思路

hasPath 的实现分为两个主要阶段:

  1. 路径逐级查找:首先将路径转换为标准数组格式,然后从第一级开始逐级检查对象中是否存在该属性。如果任一级别查找失败,立即返回 false

  2. 特殊数组处理:如果常规查找在最后一级失败,函数会进行额外检查:判断当前对象是否是数组或类数组对象,以及最后一级路径是否是一个有效的数组索引。这主要是为了处理数组中的"空位"情况。

源码解析

路径准备

path = castPath(path, object);

var index = -1,
  length = path.length,
  result = false;

这部分代码完成初始化工作:

  1. 使用 castPath 将路径转换为标准数组格式,处理不同的输入形式:

    castPath("a.b.c", obj); // 转为 ['a', 'b', 'c']
    castPath(["a", "b", "c"], obj); // 保持不变
    castPath("a.b", { "a.b": 42 }); // 转为 ['a.b'],因为这是直接属性
    
  2. 设置初始变量:

    • index: 当前处理的路径级别,初始为 -1
    • length: 路径数组的长度
    • result: 查找结果,默认为 false

路径查找循环

while (++index < length) {
  var key = toKey(path[index]);
  if (!(result = object != null && hasFunc(object, key))) {
    break;
  }
  object = object[key];
}

这个循环是函数的核心部分:

  1. ++index < length 确保循环在路径范围内进行
  2. toKey(path[index]) 将当前路径段转换为有效属性键
  3. object != null 检查当前对象是否存在(不是 nullundefined
  4. hasFunc(object, key) 使用传入的函数检查键是否存在于对象中
  5. 如果检查通过,更新 resulttrue,并深入下一层 object = object[key]
  6. 如果检查失败,设置 resultfalse 并退出循环

hasFunc 是从外部传入的属性检查函数:

  • _.has 使用 baseHas(只检查对象自身属性)
  • _.hasIn 使用 baseHasIn(检查对象自身及继承属性)

示例:

var user = {
  profile: {
    name: "张三",
  },
};

// 检查 user.profile.name 是否存在
hasPath(user, ["profile", "name"], (obj, key) => key in obj);
// 第一轮:检查 'profile' 是否存在于 user 中 ✓
// 第二轮:检查 'name' 是否存在于 user.profile 中 ✓
// 结果:true

特殊情况处理

if (result || ++index != length) {
  return result;
}
length = object == null ? 0 : object.length;
return (
  !!length &&
  isLength(length) &&
  isIndex(key, length) &&
  (isArray(object) || isArguments(object))
);

这部分处理两种情况:

  1. 如果路径查找成功(resulttrue)或未完成整个路径的遍历(++index != length),直接返回 result

  2. 如果查找恰好在最后一级失败,进行特殊检查:

    • 检查当前对象是否有 length 属性且值有效
    • 检查 length 是否是合法的数组长度
    • 检查最后一个键是否是合法的数组索引(在 0 到 length-1 范围内)
    • 确认当前对象是否是数组或类数组对象

这个特殊处理主要是为了处理数组的"空位"情况:

var arr = [];
arr[2] = "值"; // arr 现在是 [empty, empty, "值"]

// 检查 arr[1] 是否存在
hasPath(arr, [1], (obj, key) => key in obj);
// 常规检查:false(索引 1 是空的)
// 特殊检查:
// - arr.length 存在且为 3 ✓
// - 3 是合法的数组长度 ✓
// - 键 1 是合法的索引(在 0-2 范围内)✓
// - arr 是数组 ✓
// 最终结果:true

总结

hasPath 作为 Lodash 属性检查功能的核心实现,展示了几个重要的编程技巧:

  1. 输入标准化:将不同格式的输入转换为统一格式,增强函数的兼容性
  2. 逐级探索:通过循环逐步检查属性路径,发现问题立即终止,提高效率
  3. 边界情况处理:对数组的特殊处理,确保在复杂情况下也能得到符合预期的结果
  4. 函数参数化:通过传入不同的检查函数,实现灵活的属性检查策略

这些技巧在处理复杂数据结构、支持多种输入格式或需要精确控制行为时非常有用。通过 _.has_.hasIn 这两个公共 API,我们可以利用 hasPath 的功能编写更加健壮和安全的代码,避免因属性访问错误导致的程序崩溃。

每日一题-使数组元素互不相同所需的最少操作次数🟢

2025年4月8日 00:00

给你一个整数数组 nums,你需要确保数组中的元素 互不相同 。为此,你可以执行以下操作任意次:

  • 从数组的开头移除 3 个元素。如果数组中元素少于 3 个,则移除所有剩余元素。

注意:空数组也视作为数组元素互不相同。返回使数组元素互不相同所需的 最少操作次数 

 

 

示例 1:

输入: nums = [1,2,3,4,2,3,3,5,7]

输出: 2

解释:

  • 第一次操作:移除前 3 个元素,数组变为 [4, 2, 3, 3, 5, 7]
  • 第二次操作:再次移除前 3 个元素,数组变为 [3, 5, 7],此时数组中的元素互不相同。

因此,答案是 2。

示例 2:

输入: nums = [4,5,6,4,4]

输出: 2

解释:

  • 第一次操作:移除前 3 个元素,数组变为 [4, 4]
  • 第二次操作:移除所有剩余元素,数组变为空。

因此,答案是 2。

示例 3:

输入: nums = [6,7,8,9]

输出: 0

解释:

数组中的元素已经互不相同,因此不需要进行任何操作,答案是 0。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 100

CSS 渐变色

2025年4月7日 23:59

CSS 渐变(Gradients)允许你在两个或多个指定的颜色之间显示平稳的过渡。它们由浏览器生成,表现得像图像一样,通常用于 background-image 属性。

现代 CSS 中主要有三种类型的渐变:

  1. 线性渐变 (linear-gradient()): 沿直线过渡颜色。
  2. 径向渐变 (radial-gradient()): 从一个中心点向外过渡颜色。
  3. 锥形渐变 (conic-gradient()): 围绕一个中心点旋转过渡颜色(像色轮)。

此外,每种类型都有其对应的重复版本:

  • repeating-linear-gradient()
  • repeating-radial-gradient()
  • repeating-conic-gradient()

我们将深入探讨每种类型的属性、语法、技巧,并提供大量的代码示例。

适用于大多数渐变的关键概念:

  • 色标 (Color Stops):定义渐变中的颜色及其位置。位置可以是沿着渐变线(线性)、从中心向外(径向)或围绕中心(锥形)。
    • 语法:<color> [<percentage> | <length>]?
    • 示例:red, blue 50%, green 100px
    • 如果未指定位置,颜色将均匀分布。
    • 位置可以用百分比 (%) 或固定长度 (px, em 等) 指定。
    • 硬停止 (Hard Stops):如果两个色标具有相同的位置,则会创建清晰的颜色边界,而不是平滑过渡。示例:red 50%, blue 50%
    • 颜色中点/过渡提示 (Color Hints):可以建议两个色标之间过渡的中点应该在哪里。示例:red, 70%, blue(过渡中点位于 70%,而不是默认的 50%)。
  • 透明度 (Transparency):可以使用 rgba()hsla() 指定包含透明度的颜色,或者使用 transparent 关键字。这允许下方的元素或背景层透过渐变显示出来。
  • 多重渐变背景 (Multiple Gradients):可以在 background-image 属性中用逗号分隔来层叠多个渐变(以及其他背景图像)。列表中第一个渐变位于最顶层。你可以使用 background-sizebackground-positionbackground-repeat 的多值语法来分别控制每一层的尺寸、位置和重复方式。

接下来,我们将结合详细的属性和示例来分解每种类型。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS 渐变示例详解</title>
    <style>
        /* 示例盒子的基本样式 */
        body {
            font-family: sans-serif;
            padding: 20px;
            background-color: #f0f0f0;
        }

        .gradient-box {
            width: 300px;
            height: 200px;
            border: 1px solid #ccc;
            margin: 15px;
            display: inline-block;
            vertical-align: top;
            position: relative; /* 用于可能的伪元素 */
            overflow: hidden; /* 必要时裁剪内容/渐变 */
            color: #fff; /* 默认文本颜色以保证对比度 */
            font-size: 14px;
            text-align: center;
            line-height: 1.4;
        }

        .gradient-box h3 {
            margin: 5px 0;
            padding: 5px;
            background-color: rgba(0, 0, 0, 0.5); /* 半透明背景提高可读性 */
            color: white;
            font-size: 16px;
        }

        .gradient-box p {
            margin: 10px;
            padding: 5px;
            background-color: rgba(0, 0, 0, 0.4);
        }

        /* 用于较大盒子的工具类 */
        .large-box {
            width: 400px;
            height: 300px;
        }

        /* 用于文本渐变的工具类 */
        .text-gradient-demo {
            font-size: 3rem;
            font-weight: bold;
            display: inline-block; /* 对 background-clip 很重要 */
        }

        /* 特定的渐变样式从这里开始 */
        /* ----------------------------------- */

        /* === 1. 线性渐变 (`linear-gradient()`) === */

        /* 1.1 基本的从上到下 (默认) */
        .lg-basic {
            /* 默认方向是 'to bottom' (到底部) */
            background-image: linear-gradient(red, blue);
            /* 等同于: */
            /* background-image: linear-gradient(to bottom, red, blue); */
            /* 等同于: */
            /* background-image: linear-gradient(180deg, red, blue); */
        }

        /* 1.2 方向:关键字 */
        .lg-direction-keyword-lr {
            background-image: linear-gradient(to right, #ffcccc, #3366ff); /* 从左到右 */
        }

        .lg-direction-keyword-tlbr {
            background-image: linear-gradient(to bottom right, orange, purple); /* 从左上到右下 */
        }

        .lg-direction-keyword-bl {
             background-image: linear-gradient(to left, lime, cyan); /* 从右到左 */
        }

        /* 1.3 方向:角度 */
        .lg-direction-angle-45 {
            /* 0deg 指向顶部, 90deg 指向右侧, 180deg 指向底部, 270deg 指向左侧 */
            background-image: linear-gradient(45deg, #fffd80, #80ff80, #80d4ff); /* 45度角 */
        }

        .lg-direction-angle-neg30 {
            background-image: linear-gradient(-30deg, #f8a, #a8f); /* 负角度 */
        }

        /* 1.4 多个色标 */
        .lg-multiple-stops {
            /* 彩虹色,默认均匀分布 */
            background-image: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
        }

        /* 1.5 带位置的色标 (百分比) */
        .lg-stops-percent {
            background-image: linear-gradient(to right,
                red 0%,
                orange 20%,
                yellow 40%,
                green 60%,
                blue 80%,
                purple 100%
            );
        }

        /* 1.6 带位置的色标 (长度) */
        .lg-stops-length {
            height: 300px; /* 使盒子更高以观察长度效果 */
            background-image: linear-gradient(to bottom,
                #ff6347 0px,      /* Tomato 从顶部 0px 开始 */
                #ffa500 50px,     /* Orange 在 50px 处 */
                #ffd700 150px,    /* Gold 在 150px 处 */
                #90ee90 250px,    /* LightGreen 在 250px 处 */
                #add8e6 300px     /* LightBlue 在底部 300px 处 */
            );
        }

        /* 1.7 不均匀分布的色标 */
        .lg-uneven-stops {
            background-image: linear-gradient(to right, red 10%, yellow 80%, green 95%);
            /* 红色主导前10%,然后过渡到黄色直到80%,然后快速过渡到绿色直到95%,绿色填充剩余部分 */
        }

        /* 1.8 硬停止 (创建条纹) */
        .lg-hard-stops {
            background-image: linear-gradient(to right,
                red 0%, red 33.3%, /* 红色条带 */
                white 33.3%, white 66.6%, /* 白色条带 */
                blue 66.6%, blue 100% /* 蓝色条带 */
            );
        }

        .lg-hard-stops-angled {
            background-image: linear-gradient(45deg,
                #d9534f 0%, #d9534f 25%,    /* 红色 */
                #5cb85c 25%, #5cb85c 50%,    /* 绿色 */
                #5bc0de 50%, #5bc0de 75%,    /* 蓝色 */
                #f0ad4e 75%, #f0ad4e 100%     /* 橙色 */
            );
        }

        /* 1.9 使用透明度 (rgba) */
        .lg-transparency {
            background-color: #eee; /* 添加底层颜色以便观察透明效果 */
            background-image: linear-gradient(to right,
                rgba(255, 0, 0, 1),         /* 不透明红色 */
                rgba(255, 255, 0, 0.5),     /* 半透明黄色 */
                rgba(0, 0, 255, 0)          /* 完全透明蓝色 */
            );
        }

        /* 1.10 使用透明度 (transparent 关键字) */
        .lg-transparent-keyword {
            background-color: lightblue; /* 底层颜色 */
            background-image: linear-gradient(to bottom left,
                black,
                transparent 80% /* 向左下角渐变为透明 */
            );
        }

        /* 1.11 颜色中点/过渡提示 */
        .lg-color-hint {
            background-image: linear-gradient(to right,
                #ff0000, /* 红色 */
                80%,      /* 提示:过渡中点被推向蓝色 */
                #0000ff  /* 蓝色 */
            );
            /* 与默认对比: background-image: linear-gradient(to right, #ff0000, #0000ff); */
        }

        /* === 2. 重复线性渐变 (`repeating-linear-gradient()`) === */

        /* 2.1 基本的重复线性渐变 */
        .r-lg-basic {
            /* 创建每 30px 重复的条纹 */
            background-image: repeating-linear-gradient(
                to right,
                yellow,
                yellow 15px, /* 黄色持续到 15px */
                brown 15px,  /* 棕色从 15px 开始 */
                brown 30px   /* 棕色持续到 30px (一个重复单元结束) */
            );
        }

        /* 2.2 重复倾斜条纹 */
        .r-lg-angled {
            /* 创建 45 度重复条纹 */
            background-image: repeating-linear-gradient(
                45deg,
                #f0f0f0,              /* 浅灰色 */
                #f0f0f0 10px,         /* 浅灰色持续 10px */
                #e0e0e0 10px,         /* 深灰色 */
                #e0e0e0 20px          /* 深灰色持续 10px (图案总宽度 20px) */
            );
        }

        /* 2.3 重复透明条纹 (糖果棒效果) */
        .r-lg-transparent {
             background-color: white; /* 底色 */
             background-image: repeating-linear-gradient(
                -45deg,
                transparent,
                transparent 10px,            /* 透明部分 10px */
                rgba(255, 0, 0, 0.8) 10px,   /* 半透明红色开始 */
                rgba(255, 0, 0, 0.8) 20px    /* 半透明红色结束 (宽度 10px) */
            );
        }

        /* === 3. 径向渐变 (`radial-gradient()`) === */

        /* 3.1 基本径向渐变 (中心点, 椭圆, 最远角 - 默认值) */
        .rg-basic {
            /* 默认: 形状 ellipse, 大小 'farthest-corner', 位置 'center' */
            background-image: radial-gradient(red, blue);
            /* 等同于: */
            /* background-image: radial-gradient(ellipse farthest-corner at center, red, blue); */
        }

        /* 3.2 形状: Circle (圆形) */
        .rg-shape-circle {
            background-image: radial-gradient(circle, yellow, green);
            /* 圆形, 大小默认由最远角决定 */
        }

        /* 3.3 大小关键字 */
        .rg-size-closest-side {
            /* 渐变在接触到最近的边时结束 */
            background-image: radial-gradient(circle closest-side, white, black);
        }

        .rg-size-closest-corner {
            /* 渐变在接触到最近的角时结束 */
            background-image: radial-gradient(ellipse closest-corner, #ffdddd, #880000);
        }

        .rg-size-farthest-side {
             /* 渐变在接触到最远的边时结束 */
            background-image: radial-gradient(circle farthest-side, cyan, navy);
        }

        .rg-size-farthest-corner {
            /* 渐变在接触到最远的角时结束 (默认) */
            background-image: radial-gradient(ellipse farthest-corner, lime, darkgreen);
        }

        /* 3.4 显式大小: 长度 (圆形半径) */
        .rg-size-length-circle {
            background-image: radial-gradient(circle 50px, yellow, orange); /* 半径为 50px 的圆形 */
        }

        /* 3.5 显式大小: 长度/百分比 (椭圆半径) */
        .rg-size-length-ellipse {
            background-image: radial-gradient(ellipse 100px 50px, lightblue, darkblue); /* 椭圆,宽100px, 高50px */
        }

        .rg-size-percent-ellipse {
             background-image: radial-gradient(ellipse 60% 30%, #ffffcc, #cccc00); /* 椭圆,宽60%, 高30% */
        }

        /* 3.6 位置: 关键字 */
        .rg-position-keyword {
            /* 定位在左上角 */
            background-image: radial-gradient(circle at top left, red, transparent);
            background-color: #eee; /* 显示透明效果 */
        }

        .rg-position-keyword-bottom {
             background-image: radial-gradient(ellipse at bottom, black, grey); /* 定位在底部中心 */
        }

        /* 3.7 位置: 长度/百分比 */
        .rg-position-values {
            /* 定位在距离左边 50px, 距离顶部 30% 的地方 */
            background-image: radial-gradient(circle at 50px 30%, #f0f, #a0a);
        }

        /* 3.8 带位置的多个色标 */
        .rg-multiple-stops {
            background-image: radial-gradient(circle,
                red 10%,      /* 红色实心圆,半径到 10% */
                yellow 30%,   /* 在 10% 到 30% 之间从红色过渡到黄色 */
                blue 80%      /* 在 30% 到 80% 之间从黄色过渡到蓝色,蓝色填充剩余部分 */
            );
        }

        /* 3.9 硬停止 (创建圆环) */
        .rg-hard-stops {
            background-image: radial-gradient(circle,
                red 0%, red 20px,           /* 红色内圈 (半径 0-20px) */
                white 20px, white 40px,     /* 白色圆环 (半径 20-40px) */
                blue 40px, blue 60px,       /* 蓝色圆环 (半径 40-60px) */
                transparent 60px           /* 外部透明 */
            );
            background-color: #ddd; /* 显示透明效果 */
        }

        /* 3.10 透明度 */
        .rg-transparency {
            background-color: lightcoral; /* 底色 */
            background-image: radial-gradient(circle at center,
                rgba(255, 255, 255, 0.8) 0%,  /* 中心半透明白色 */
                rgba(255, 255, 255, 0) 70%   /* 向外渐变为完全透明 */
            );
        }

        /* === 4. 重复径向渐变 (`repeating-radial-gradient()`) === */

        /* 4.1 基本的重复径向渐变 */
        .r-rg-basic {
            /* 每 20px 重复一次黑白圆环 */
            background-image: repeating-radial-gradient(circle,
                black,
                black 10px, /* 黑色到 10px */
                white 10px, /* 白色从 10px 开始 */
                white 20px  /* 白色到 20px (一个重复单元结束) */
            );
        }

        /* 4.2 带位置和大小的重复径向渐变 */
        .r-rg-positioned {
            background-image: repeating-radial-gradient(circle closest-side at 20% 30%, /* 圆形, 最近边, 中心在 20% 30% */
                #3f87a6,
                #3f87a6 10px,
                #ebf8e1 10px,
                #ebf8e1 20px /* 每 20px 重复 */
            );
        }

        /* 4.3 带透明度的重复径向渐变 (靶心效果) */
        .r-rg-transparent {
            background-color: gold; /* 底色 */
            background-image: repeating-radial-gradient(circle,
                transparent 0,
                transparent 15px,            /* 透明圆心 (半径 0-15px) */
                rgba(0, 0, 0, 0.5) 15px,     /* 半透明黑色圆环开始 */
                rgba(0, 0, 0, 0.5) 30px      /* 半透明黑色圆环结束 (宽度 15px) */
            );
        }


        /* === 5. 锥形渐变 (`conic-gradient()`) === */
        /* 注意: 浏览器支持良好,但可能比线性和径向的普及度稍低 */

        /* 5.1 基本锥形渐变 (默认: 从 0deg 开始, 在中心点) */
        .cg-basic {
            background-image: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
            /* 颜色围绕中心点平滑过渡 */
        }

        /* 5.2 指定起始角度 (`from`) */
        .cg-from-angle {
            /* 从 90 度 (顶部中心) 开始渐变 */
            background-image: conic-gradient(from 90deg, red, blue);
        }

        /* 5.3 指定中心位置 (`at`) */
        .cg-at-position {
             /* 将渐变中心定位在距离左边 25%, 距离顶部 50% 的地方 */
            background-image: conic-gradient(at 25% 50%, white, black);
        }

        /* 5.4 带角度的色标 */
        .cg-stops-angle {
            background-image: conic-gradient(
                red 0deg,         /* 红色从 0 度 (右侧) 开始 */
                yellow 90deg,     /* 到 90 度 (顶部) 过渡到黄色 */
                blue 180deg,      /* 到 180 度 (左侧) 过渡到蓝色 */
                red 360deg        /* 回到红色完成圆周 */
            );
        }

        /* 5.5 带百分比的色标 */
        .cg-stops-percent {
            background-image: conic-gradient(
                red 0%,         /* 红色从 0% 开始 */
                yellow 25%,     /* 到 25% (90deg) 过渡到黄色 */
                blue 50%,      /* 到 50% (180deg) 过渡到蓝色 */
                red 100%        /* 回到红色完成圆周 */
            );
        }

        /* 5.6 硬停止 (饼图) */
        .cg-hard-stops-pie {
            background-image: conic-gradient(
                #ff6347 0deg 90deg,   /* Tomato - 第 1 象限 (25%) */
                #90ee90 90deg 180deg,  /* LightGreen - 第 2 象限 (25%) */
                #add8e6 180deg 270deg, /* LightBlue - 第 3 象限 (25%) */
                #ffd700 270deg 360deg   /* Gold - 第 4 象限 (25%) */
            );
            border-radius: 50%; /* 使其看起来像饼图 */
            width: 200px; height: 200px; /* 等宽高以形成圆形 */
        }

        /* 5.7 硬停止 (棋盘格) */
        .cg-hard-stops-checkerboard {
            background-color: #eee; /* 底色 */
            /* 图案每 90 度重复一次。黑/白方块。 */
            background-image: conic-gradient(
                black 0deg 90deg,          /* 黑色从 0 到 90 度 */
                white 90deg 180deg,        /* 白色从 90 到 180 度 */
                black 180deg 270deg,       /* 黑色从 180 到 270 度 */
                white 270deg 360deg        /* 白色从 270 到 360 度 */
            );
            /* 设置重复单元的大小 */
            background-size: 80px 80px; /* 一个 2x2 棋盘格方块的大小 */
        }

        /* 5.8 透明度 */
        .cg-transparency {
            background-color: lightpink; /* 底色 */
            background-image: conic-gradient(
                from 45deg,
                rgba(0, 0, 0, 1) 0deg 90deg,   /* 不透明黑色 */
                rgba(0, 0, 0, 0) 90deg 180deg, /* 渐变到透明 */
                rgba(0, 0, 0, 1) 180deg 270deg, /* 不透明黑色 */
                rgba(0, 0, 0, 0) 270deg 360deg  /* 渐变到透明 */
            );
            border-radius: 50%;
            width: 200px; height: 200px;
        }

        /* === 6. 重复锥形渐变 (`repeating-conic-gradient()`) === */

        /* 6.1 基本的重复锥形渐变 (星芒效果) */
        .r-cg-basic {
             /* 重复一个 45 度的扇区 8 次 */
            background-image: repeating-conic-gradient(
                black 0deg 15deg,   /* 黑色 0-15 度 */
                white 15deg 30deg,  /* 白色 15-30 度 */
                grey 30deg 45deg    /* 灰色 30-45 度 (一个重复单元结束) */
            );
             border-radius: 50%;
             width: 200px; height: 200px;
        }

        /* 6.2 带透明度的重复锥形渐变 */
        .r-cg-transparent {
            background-color: teal; /* 底色 */
            background-image: repeating-conic-gradient(
                from 10deg,
                transparent 0deg 5deg,          /* 透明间隙 (0-5度) */
                rgba(255, 255, 255, 0.7) 5deg 20deg /* 半透明白色扇区 (5-20度) */
            );
            /* 图案每 20 度重复一次 */
            border-radius: 50%;
            width: 200px; height: 200px;
        }

        /* === 7. 高级技巧与实用示例 === */

        /* 7.1 多背景层叠 (网格图案) */
        .adv-layering-pattern {
            background-color: #6a8ee8; /* 底色 */
            /* 顶层: 白色水平线 */
            /* 底层: 白色垂直线 */
            background-image:
                repeating-linear-gradient( /* 水平线 */
                    to bottom,               /* 方向 */
                    transparent,             /* 透明 */
                    transparent 5px,         /* 透明持续 5px */
                    rgba(255, 255, 255, 0.5) 5px, /* 白色线开始 */
                    rgba(255, 255, 255, 0.5) 10px /* 白色线结束 (5px 粗) */
                ),
                repeating-linear-gradient( /* 垂直线 */
                    to right,               /* 方向 */
                    transparent,            /* 透明 */
                    transparent 5px,        /* 透明持续 5px */
                    rgba(255, 255, 255, 0.5) 5px, /* 白色线开始 */
                    rgba(255, 255, 255, 0.5) 10px /* 白色线结束 (5px 粗) */
                );
             /* repeating-linear-gradient 会自动填充空间,通常无需 background-repeat */
             /* 如果需要,可以用 background-size / background-position 控制 */
        }

        /* 7.2 多背景层叠 (细微纹理) */
        .adv-layering-texture {
            background-color: #444; /* 底色 */
            background-image:
                /* 顶层: 使用径向渐变制造微妙噪点 */
                radial-gradient(circle at 1px 1px, rgba(255,255,255,0.1) 1px, transparent 0),
                /* 底层: 非常细微的垂直渐变 */
                linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.3));
            /* 控制径向 '噪点' 层 */
            background-size: 3px 3px, auto; /* 平铺 3x3 的径向渐变 */
            background-position: 0 0, 0 0;
            background-repeat: repeat, no-repeat;
        }


        /* 7.3 渐变文字 */
        .adv-text-gradient {
            /* 将渐变应用为背景 */
            background-image: linear-gradient(45deg, #ff8a00, #e52e71);

            /* 使用 background-clip 将背景裁切到文字形状 */
            -webkit-background-clip: text; /* Safari/Chrome 前缀 */
            background-clip: text;

            /* 将文字颜色设为透明,让背景渐变显示出来 */
            color: transparent;

            /* 为不支持 background-clip: text 的浏览器提供后备文字颜色 */
            /* (可选但推荐) */
            color: #e52e71;
        }

        /* 使后备方案更健壮 */
        @supports (-webkit-background-clip: text) or (background-clip: text) {
          .adv-text-gradient {
            color: transparent; /* 仅在支持时将颜色设为透明 */
          }
        }

        /* 7.4 渐变边框 (使用 `border-image`) */
        .adv-border-image {
            width: 280px; height: 180px; /* 调整大小 */
            border: 10px solid transparent; /* 必须有边框宽度和 solid 样式 */
                                            /* 颜色通常设为 transparent */

            /* 定义渐变源 */
            border-image-source: linear-gradient(to right bottom, #f0ad4e, #d9534f, #5cb85c);

            /* 切割渐变图像。'1' 表示使用完整的边框宽度。 */
            /* 数字对应上、右、下、左切割线 */
            border-image-slice: 1;

             /* 如何填充边框图像区域 (stretch, repeat, round) */
             /* border-image-fill: BORDER_IMAGE_FILL; */ /* 可选: 'fill' 会使渐变也覆盖内容区域 */
             /* border-image-repeat: BORDER_IMAGE_REPEAT; */ /* 可选: 'stretch'(默认), 'repeat', 'round', 'space' */
             /* border-image-width: BORDER_IMAGE_WIDTH; */ /* 可选: 默认等于 border-width */
             /* border-image-outset: BORDER_IMAGE_OUTSET; */ /* 可选: 将边框图像向外推 */
        }
        /* 注意: border-image 简写属性很复杂,通常使用单独属性更好 */

        /* 7.5 渐变边框 (替代方案: Background Origin/Clip) - 更灵活 */
        .adv-border-background {
            width: 280px; height: 180px; /* 调整大小 */
            border: 10px solid transparent; /* 透明边框 */
            background-color: white; /* 内容区域的背景色 */

            /* 应用到元素的渐变 */
            background-image:
                linear-gradient(white, white), /* 内层背景 (纯白) */
                linear-gradient(to right bottom, #5bc0de, #6a8ee8, #f0f); /* 外层渐变 (边框区域) */

            /* 控制背景绘制的起始位置 */
            background-origin: border-box, border-box; /* 都从边框边缘开始绘制 */

            /* 控制背景的裁切区域 */
            /* 内层背景裁切到 padding-box (边框内部) */
            /* 外层渐变裁切到 border-box (覆盖边框区域) */
            background-clip: padding-box, border-box;
        }

        /* 7.6 使用渐变进行遮罩 (`mask-image` / `-webkit-mask-image`) */
        .adv-mask-image {
            width: 300px;
            height: 200px;
            background: url('https://via.placeholder.com/300x200/0000FF/808080?text=被遮罩的图像') center/cover no-repeat; /* 示例图片 */

            /* 遮罩层: 黑色区域不透明, 白色区域透明, 灰色区域半透明 */
            /* 我们使用从黑色 (可见) 到透明 (不可见) 的渐变 */
            mask-image: linear-gradient(to bottom,
                black 60%,      /* 顶部 60% 完全可见 */
                transparent 100% /* 到底部渐变为不可见 */
            );

            /* 需要厂商前缀以获得更广泛的兼容性 */
            -webkit-mask-image: linear-gradient(to bottom,
                black 60%,
                transparent 100%
            );

            /* 可选的遮罩属性 */
            /* mask-size: cover; */
            /* mask-position: center; */
            /* mask-repeat: no-repeat; */
        }

        /* 7.7 动画渐变背景 (注意性能!) */
        @keyframes animated-gradient {
            0% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
            100% { background-position: 0% 50%; }
        }

        .adv-animated-gradient {
            background-image: linear-gradient(90deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
            background-size: 400% 400%; /* 使渐变比容器大 */
            animation: animated-gradient 15s ease infinite;
        }
        /* 注意:直接在 @keyframes 中改变渐变颜色通常性能不佳。*/
        /* 对一个较大的渐变背景进行 background-position 动画通常是更好的选择。*/
        /* 使用 CSS 自定义属性 (CSS Variables) 也是一种管理方式。 */

        /* 7.8 按钮悬停效果 */
        .button-gradient-hover {
            padding: 15px 30px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            color: white;
            background-image: linear-gradient(to right, #6a11cb 0%, #2575fc 100%);
            background-size: 200% auto; /* 双倍宽度 */
            background-position: 0% center; /* 从左侧开始 */
            transition: background-position 0.5s ease;
            display: inline-block; /* 需要盒模型 */
            text-align: center;
        }

        .button-gradient-hover:hover {
            background-position: 100% center; /* 悬停时将背景移动到右侧 */
        }

        /* 7.9 使用重复渐变的细微背景图案 */
        .adv-subtle-pattern {
            height: 300px; /* 加高以看清图案 */
            background-color: #f8f9fa; /* 浅色底 */
            background-image:
                repeating-linear-gradient(45deg, #e9ecef 0, #e9ecef 1px, transparent 1px, transparent 10px), /* 细对角线 */
                repeating-linear-gradient(-45deg, #e9ecef 0, #e9ecef 1px, transparent 1px, transparent 10px); /* 细反对角线 */
            /* 创建一个细微的交叉影线图案 */
        }

        /* 7.10 复杂层叠场景 (例如简单的天空和地面) */
        .adv-scene {
             height: 300px;
             background-color: #87CEEB; /* 后备天蓝色 */
             background-image:
                /* 太阳 (径向) */
                radial-gradient(circle at 80% 20%, yellow 5px, orange 15px, rgba(255,165,0,0) 50px),
                /* 天空 (线性) */
                linear-gradient(to bottom, #87CEEB 0%, #add8e6 70%),
                /* 地面 (线性 - 定位在下方) */
                linear-gradient(to bottom, #228B22 0%, #006400 100%);

            /* 控制层 */
            background-repeat: no-repeat, no-repeat, no-repeat; /* 所有层都不重复 */
            background-position: 0 0, 0 0, 0 70%; /* 地面从距离顶部 70% 的位置开始 */
            background-size: auto, auto, 100% 30%; /* 地面大小为宽度 100%, 高度 30% */
        }

        /* === 8. 可访问性注意事项 === */
        .accessibility-contrast {
            background-image: linear-gradient(to right, white, black);
            color: red; /* 红色文本在渐变的某些部分对比度很差 */
        }
        .accessibility-contrast-ok {
             background-image: linear-gradient(to right, #003366, #005599); /* 深蓝色渐变 */
             color: white; /* 白色文本在这里通常有良好的对比度 */
        }
        /* 务必检查文本颜色与渐变背景各个部分的对比度 */
        /* 浏览器开发者工具或在线对比度检查器是必不可少的工具。 */


        /* === 9. 浏览器前缀 (现在需求减少,但了解有益) === */
        .legacy-prefixes {
            /* 针对旧 Webkit 浏览器的示例 */
            /* background-image: -webkit-linear-gradient(left, red, blue); */ /* 旧语法 */
            /* background-image: -webkit-linear-gradient(to right, red, blue); */ /* 较新的前缀语法 */

            /* 标准语法应始终放在最后 */
            background-image: linear-gradient(to right, red, blue);
        }
        /* 现代构建工具通常会自动处理前缀。*/
        /* 通常只在需要支持非常旧的浏览器 (例如 Android 4.x, 旧版 iOS) 时才需要手动添加。 */

    </style>
</head>
<body>

    <h1>CSS 渐变示例详解</h1>

    <h2>1. 线性渐变 (`linear-gradient()`)</h2>
    <div class="gradient-box lg-basic"><h3>1.1 基本 (从上到下)</h3><p>linear-gradient(red, blue)</p></div>
    <div class="gradient-box lg-direction-keyword-lr"><h3>1.2 方向 (从左到右)</h3><p>linear-gradient(to right, ...)</p></div>
    <div class="gradient-box lg-direction-keyword-tlbr"><h3>1.2 方向 (左上到右下)</h3><p>linear-gradient(to bottom right, ...)</p></div>
    <div class="gradient-box lg-direction-keyword-bl"><h3>1.2 方向 (从右到左)</h3><p>linear-gradient(to left, ...)</p></div>
    <div class="gradient-box lg-direction-angle-45"><h3>1.3 方向 (角度 45deg)</h3><p>linear-gradient(45deg, ...)</p></div>
    <div class="gradient-box lg-direction-angle-neg30"><h3>1.3 方向 (角度 -30deg)</h3><p>linear-gradient(-30deg, ...)</p></div>
    <div class="gradient-box lg-multiple-stops"><h3>1.4 多个色标</h3><p>彩虹色 (均匀分布)</p></div>
    <div class="gradient-box lg-stops-percent"><h3>1.5 带 % 的色标</h3><p>red 0%, orange 20%, ...</p></div>
    <div class="gradient-box lg-stops-length"><h3>1.6 带长度的色标</h3><p>#ff6347 0px, #ffa500 50px, ...</p></div>
    <div class="gradient-box lg-uneven-stops"><h3>1.7 不均匀色标</h3><p>red 10%, yellow 80%, green 95%</p></div>
    <div class="gradient-box lg-hard-stops"><h3>1.8 硬停止 (条纹)</h3><p>red 33.3%, white 33.3%, ...</p></div>
    <div class="gradient-box lg-hard-stops-angled"><h3>1.8 硬停止 (倾斜)</h3><p>45度硬停止条纹</p></div>
    <div class="gradient-box lg-transparency"><h3>1.9 透明度 (rgba)</h3><p>红到透明蓝,底层 #eee</p></div>
    <div class="gradient-box lg-transparent-keyword"><h3>1.10 透明度 (关键字)</h3><p>黑到 transparent,底层 lightblue</p></div>
    <div class="gradient-box lg-color-hint"><h3>1.11 颜色中点</h3><p>linear-gradient(to right, red, 80%, blue)</p></div>

    <h2>2. 重复线性渐变 (`repeating-linear-gradient()`)</h2>
    <div class="gradient-box r-lg-basic"><h3>2.1 基本重复</h3><p>黄/棕条纹,每 30px 重复</p></div>
    <div class="gradient-box r-lg-angled"><h3>2.2 重复倾斜</h3><p>45度灰色条纹,每 20px 重复</p></div>
    <div class="gradient-box r-lg-transparent"><h3>2.3 重复透明</h3><p>糖果棒效果</p></div>

    <h2>3. 径向渐变 (`radial-gradient()`)</h2>
    <div class="gradient-box rg-basic"><h3>3.1 基本径向 (椭圆)</h3><p>radial-gradient(red, blue)</p></div>
    <div class="gradient-box rg-shape-circle"><h3>3.2 形状: 圆形</h3><p>radial-gradient(circle, ...)</p></div>
    <div class="gradient-box rg-size-closest-side"><h3>3.3 大小: 最近边</h3><p>circle closest-side, ...</p></div>
    <div class="gradient-box rg-size-closest-corner"><h3>3.3 大小: 最近角</h3><p>ellipse closest-corner, ...</p></div>
    <div class="gradient-box rg-size-farthest-side"><h3>3.3 大小: 最远边</h3><p>circle farthest-side, ...</p></div>
    <div class="gradient-box rg-size-farthest-corner"><h3>3.3 大小: 最远角</h3><p>ellipse farthest-corner, ...</p></div>
    <div class="gradient-box rg-size-length-circle"><h3>3.4 大小: 长度 (圆)</h3><p>circle 50px, ...</p></div>
    <div class="gradient-box rg-size-length-ellipse"><h3>3.5 大小: 长度 (椭圆)</h3><p>ellipse 100px 50px, ...</p></div>
     <div class="gradient-box rg-size-percent-ellipse"><h3>3.5 大小: 百分比 (椭圆)</h3><p>ellipse 60% 30%, ...</p></div>
    <div class="gradient-box rg-position-keyword"><h3>3.6 位置: 关键字</h3><p>circle at top left, ...</p></div>
    <div class="gradient-box rg-position-keyword-bottom"><h3>3.6 位置: 关键字</h3><p>ellipse at bottom, ...</p></div>
    <div class="gradient-box rg-position-values"><h3>3.7 位置: 值</h3><p>circle at 50px 30%, ...</p></div>
    <div class="gradient-box rg-multiple-stops"><h3>3.8 多个色标 %</h3><p>red 10%, yellow 30%, blue 80%</p></div>
    <div class="gradient-box rg-hard-stops"><h3>3.9 硬停止 (圆环)</h3><p>red 20px, white 40px, blue 60px</p></div>
    <div class="gradient-box rg-transparency"><h3>3.10 透明度</h3><p>半透明白色中心淡出</p></div>

    <h2>4. 重复径向渐变 (`repeating-radial-gradient()`)</h2>
    <div class="gradient-box r-rg-basic"><h3>4.1 基本重复</h3><p>重复黑白圆环</p></div>
    <div class="gradient-box r-rg-positioned"><h3>4.2 重复定位</h3><p>重复圆环,偏移中心</p></div>
    <div class="gradient-box r-rg-transparent"><h3>4.3 重复透明</h3><p>靶心效果,底层 gold</p></div>

    <h2>5. 锥形渐变 (`conic-gradient()`)</h2>
    <div class="gradient-box cg-basic"><h3>5.1 基本锥形</h3><p>conic-gradient(red, ..., red)</p></div>
    <div class="gradient-box cg-from-angle"><h3>5.2 起始角度</h3><p>conic-gradient(from 90deg, ...)</p></div>
    <div class="gradient-box cg-at-position"><h3>5.3 中心位置</h3><p>conic-gradient(at 25% 50%, ...)</p></div>
    <div class="gradient-box cg-stops-angle"><h3>5.4 带角度色标</h3><p>red 0deg, yellow 90deg, ...</p></div>
    <div class="gradient-box cg-stops-percent"><h3>5.5 带 % 色标</h3><p>red 0%, yellow 25%, ...</p></div>
    <div class="gradient-box cg-hard-stops-pie"><h3>5.6 硬停止 (饼图)</h3><p>4 个象限,border-radius 50%</p></div>
    <div class="gradient-box cg-hard-stops-checkerboard"><h3>5.7 硬停止 (棋盘格)</h3><p>需要 background-size</p></div>
    <div class="gradient-box cg-transparency"><h3>5.8 透明度</h3><p>交替不透明/透明扇区</p></div>

    <h2>6. 重复锥形渐变 (`repeating-conic-gradient()`)</h2>
    <div class="gradient-box r-cg-basic"><h3>6.1 基本重复 (星芒)</h3><p>重复 45 度扇区</p></div>
    <div class="gradient-box r-cg-transparent"><h3>6.2 重复透明</h3><p>透明间隙产生 '射线' 效果</p></div>

    <h2>7. 高级技巧与实用示例</h2>
    <div class="gradient-box large-box adv-layering-pattern"><h3>7.1 层叠 (网格图案)</h3><p>重复水平/垂直线</p></div>
    <div class="gradient-box large-box adv-layering-texture"><h3>7.2 层叠 (细微纹理)</h3><p>噪点 + 微妙暗角</p></div>
    <div class="gradient-box">
        <h3>7.3 渐变文字</h3>
        <span class="text-gradient-demo adv-text-gradient">渐变文字</span>
    </div>
    <div class="gradient-box adv-border-image"><h3>7.4 渐变边框 (border-image)</h3><p>使用 border-image-source</p></div>
    <div class="gradient-box adv-border-background"><h3>7.5 渐变边框 (background-clip)</h3><p>更灵活的替代方案</p></div>
    <div class="gradient-box adv-mask-image"><h3>7.6 使用渐变遮罩</h3><p>图像底部淡出</p></div>
    <div class="gradient-box adv-animated-gradient"><h3>7.7 动画渐变</h3><p>移动 background-position (性能!)</p></div>
    <div class="gradient-box">
        <h3>7.8 按钮悬停</h3>
        <span class="button-gradient-hover">悬停我</span>
    </div>
    <div class="gradient-box large-box adv-subtle-pattern"><h3>7.9 细微背景图案</h3><p>重复对角线</p></div>
    <div class="gradient-box large-box adv-scene"><h3>7.10 复杂场景</h3><p>太阳、天空、地面层叠</p></div>


    <h2>8. 可访问性注意事项</h2>
    <div class="gradient-box accessibility-contrast"><h3>8.1 对比度差</h3><p>红色文本在黑白渐变上部分难读</p></div>
     <div class="gradient-box accessibility-contrast-ok"><h3>8.2 对比度较好</h3><p>白色文本在深蓝渐变上通常可读</p></div>

    <h2>9. 浏览器前缀</h2>
    <div class="gradient-box legacy-prefixes"><h3>9.1 旧版前缀</h3><p>如果需要,标准语法应放最后</p></div>

</body>
</html>

中文小结与解释:

  1. HTML 结构: 设置了带有特定类名的 div 元素,用于应用不同的渐变样式。包含标题和段落以提供上下文信息。
  2. 基本样式: 为演示框 (.gradient-box) 提供了宽度、高度、边框和边距等基础样式。
  3. 线性渐变 (linear-gradient):
    • 演示了默认方向 (to bottom,即从上到下)。
    • 展示了关键字方向(to right, to bottom right 等)和角度方向(45deg, -30deg)。
    • 说明了多个色标的用法,包括均匀分布和指定百分比/长度位置的情况。
    • 解释了硬停止(Hard Stops)如何创建实色条带。
    • 展示了如何使用 rgba()transparent 实现透明效果。
    • 包含了一个颜色中点(Color Hint)的示例,用于控制过渡的中心点。
  4. 重复线性渐变 (repeating-linear-gradient): 展示了如何创建水平、倾斜以及带透明度的重复图案(如条纹)。
  5. 径向渐变 (radial-gradient):
    • 涵盖了基本语法和默认行为(椭圆、最远角、中心点)。
    • 展示了如何指定形状 (circle,圆形)。
    • 解释了大小关键字(closest-side, farthest-corner 等)和显式大小(长度、百分比)。
    • 演示了使用关键字(top left)和值(50px 30%)进行定位。
    • 包含了多个色标、硬停止(圆环)和透明度的示例。
  6. 重复径向渐变 (repeating-radial-gradient): 创建重复同心圆环的示例,包括定位和透明变化。
  7. 锥形渐变 (conic-gradient):
    • 介绍了围绕中心点进行颜色过渡的基本语法。
    • 展示了如何设置起始角度 (from) 和中心位置 (at)。
    • 说明了使用角度 (deg) 和百分比 (%) 定义色标。
    • 演示了硬停止的应用,如创建饼图和棋盘格(结合 background-size)。
    • 包含了透明度的示例。
  8. 重复锥形渐变 (repeating-conic-gradient): 展示了如何创建重复的楔形图案(如星芒效果)以及使用透明度。
  9. 高级技巧 (Advanced Techniques):
    • 层叠 (Layering): 在 background-image 中用逗号分隔组合多个渐变(线性/径向/锥形),创建复杂的图案和纹理。需要为每一层仔细设置 background-size, background-position, background-repeat
    • 渐变文字 (Gradient Text): 使用 background-image, background-clip: text, 和 color: transparent 实现。包含了后备方案和 @supports 检查。
    • 渐变边框 (Gradient Borders): 展示了两种方法:border-image(可能较复杂)和更灵活的 background-clip: padding-box, border-box 技术。
    • 遮罩 (Masking): 使用 mask-image(及 -webkit-mask-image)配合渐变来选择性地隐藏元素的一部分(例如,使图片淡出)。
    • 动画 (Animation): 通过动画化超大渐变的 background-position 来实现平滑效果,优于直接动画化渐变颜色(强调了性能考量)。
    • 按钮悬停 (Button Hover): 一种常见的 UI 技巧,使用 background-sizebackground-position 的过渡。
    • 细微图案 (Subtle Patterns): 使用微弱的重复渐变创建背景纹理。
    • 复杂场景 (Complex Scenes): 层叠不同类型和位置的渐变来构建简单的图形场景。
  10. 可访问性 (Accessibility): 简要说明了检查文本与渐变背景之间对比度的重要性。
  11. 浏览器前缀 (Vendor Prefixes): 提到了历史上需要添加前缀(-webkit-, -moz- 等),但指出现在已不那么关键,不过标准语法应始终放在最后。

配置 Gemini Code Assist 插件

2025年4月7日 23:23

1. 设置使用的模型 (Setting the Model Used)

  • 通常情况:用户无法直接选择
    • 对于标准的 Gemini Code Assist 插件(无论是 VS Code 还是 JetBrains IDEs),用户通常不能在插件设置中直接选择要使用的具体 Gemini 模型版本(例如 Gemini 1.0 Pro, Gemini 1.5 Pro, Gemini Ultra 等)。
    • Google 会根据任务类型、用户订阅(如果是企业版或与 Google Cloud 项目关联)、以及后端优化,自动选择最适合代码辅助任务的模型。其目标是提供最佳的性能和代码相关能力。
  • 可能的间接影响因素:
    • Google Cloud 项目/订阅: 如果你使用的是与特定 Google Cloud 项目关联的企业版 Gemini Code Assist,你项目所拥有的权限和订阅级别可能会间接影响可用的模型能力,但这仍然不是在 IDE 插件设置里直接选择的。
    • 插件版本: Google 会不断更新插件和后端模型,确保用户使用的是较新和优化的版本。
  • 总结: 你不需要(通常也不能)在插件设置中手动选择模型。系统会自动处理。

2. 设置每次都是中文回复 (Setting Replies to Always be in Chinese)

  • 最可靠的方法:在提问时明确指示
    • 目前,Gemini Code Assist 插件本身没有一个全局设置项让你“锁定”回复语言为中文。
    • 最有效的方法是,在你每次与 Gemini 交互(提问、请求解释、请求代码生成等)时,用中文提问,或者在你的问题中明确要求用中文回复
    • 示例:
      • 直接用中文提问:“请解释一下这段 Python 代码的作用。”
      • 如果用英文提问,可以加上说明:“Explain this Python code. Please reply in Chinese.” 或者 "请用中文解释以上代码。"
  • 为什么没有全局设置?
    • AI 模型通常是多语言的,它们会根据你的输入语言来判断输出语言。强制设定单一输出语言可能会限制其灵活性。
    • 通过 prompt(提示/问题)来控制输出语言是目前大型语言模型最常见的交互方式。
  • 总结: 养成用中文提问或在提问时加上“请用中文回复”的习惯,是确保获得中文答案的最佳方式。

3. 添加当前文件和文件夹为上下文 (Adding Current File and Folder as Context)

  • 当前文件 (Current File):
    • Gemini Code Assist 默认会以你当前编辑器中打开并处于活动状态的文件作为主要上下文
    • 当你选中代码片段进行操作(如右键 -> Gemini -> Explain This),或者在聊天窗口中提问时没有明确指定其他文件,插件通常会自动关联到当前文件。
    • 你不需要进行特殊设置来“添加”当前文件,这是它的基本工作方式。
  • 特定文件或整个工作区/文件夹 (Specific Files or Entire Workspace/Folder):
    • 使用 @ 符号 (Using the @ symbol): 现代的 AI 代码助手(包括 Gemini)通常支持在聊天界面中使用 @ 符号来引用上下文:
      • @workspace@项目: 通常可以用来指示 Gemini 考虑整个项目/工作区的上下文。这对于询问关于项目结构、跨文件依赖等问题很有用。
      • @文件名.扩展名 (例如 @main.py, @src/utils.js): 可以用来明确告诉 Gemini 参考某个特定的文件,即使它当前没有打开。
    • 隐式上下文 (Implicit Context): 当你在一个项目文件夹中使用 Gemini Code Assist 时,它本身就对项目的文件结构有一定程度的感知。当你问及与项目相关的问题时(即使没有明确用 @workspace),它也会尝试利用项目信息。
    • 选中代码 (Code Selection): 始终可以选择你关心的代码片段,然后通过右键菜单或快捷键调用 Gemini 功能,这样上下文就非常明确地聚焦于你选中的部分。
  • 如何找到插件设置 (How to Find Plugin Settings):
    • VS Code:
      1. 点击左侧边栏的“扩展”图标 (Extensions)。
      2. 找到已安装的 "Gemini Code Assist" (或类似名称,如 Google - Gemini)。
      3. 点击它旁边的齿轮图标 (⚙️)。
      4. 选择“扩展设置” (Extension Settings)。
    • JetBrains IDEs (IntelliJ IDEA, PyCharm, etc.):
      1. 打开设置/首选项 (File -> Settings... on Windows/Linux, or [IDE Name] -> Settings/Preferences... on macOS)。
      2. 导航到 "Tools" -> "Google Gemini" (或类似路径)。

总结:

  • 模型选择: 通常由 Google 自动管理,用户无需设置。
  • 中文回复: 在提问时用中文或明确要求用中文回复。
  • 上下文:
    • 当前文件自动作为上下文。
    • 使用 @workspace 指代整个项目/文件夹。
    • 使用 @文件名 指代特定文件。
    • 插件会对当前项目结构有基本的感知。

react基础2

2025年4月7日 22:26

一、React 的三大核心特点是什么?

  1. 声明式编程‌:通过JSX描述UI最终状态,开发者无需操作DOM细节(如<div>{count}</div>);
  2. 组件化‌:将UI拆分为独立可复用的组件(如函数组件、类组件);
  3. 一次学习,多端编写‌:支持Web、Native(React Native)、SSR等场景。

二、React 中如何实现条件渲染?

  1. JS表达式‌:在JSX中直接使用逻辑运算符
{isLoggedIn && <UserPanel />}
  1. 变量存储元素‌:
let message = null;
if (error) message = <ErrorBox />;
return <div>{message}</div>;
  1. 三元运算符‌:
{score > 60 ? <Pass /> : <Fail />}

三、什么是 React 的合成事件(SyntheticEvent)?

React 封装浏览器原生事件,提供跨浏览器一致的事件对象。
特点‌:

  • 事件委托:所有事件冒泡到顶层document处理,提升性能;
  • 自动回收:事件对象在回调结束后会被清除,需用e.persist()手动保留;
  • 统一API:如onClick对应click事件,onChange统一处理表单输入。

四、如何用 PropTypes 进行类型检查?

  1. 安装prop-types库;
  2. 在类组件中定义静态属性:
import PropTypes from 'prop-types';

class Button extends React.Component {
  static propTypes = {
    color: PropTypes.oneOf(['red', 'blue']).isRequired,
    onClick: PropTypes.func
  };
}
  1. 函数组件中使用:
function Button({ color }) { ... }
Button.propTypes = { color: PropTypes.string };

五、React 组件是什么?

React 组件是一个接收props(输入参数)并返回描述UI的React元素(输出)的函数或类。

// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

六、React.Fragment 的作用是什么?

用于包裹多个子元素而不添加额外DOM节点:

function Table() {
  return (
    <React.Fragment>
      <td>Cell 1</td>
      <td>Cell 2</td>
    </React.Fragment>
  );
}
// 简写语法:
<>
  <td>Cell 1</td>
  <td>Cell 2</td>
</>

七、React Hooks 的使用规则有哪些?

  1. 顶层调用‌:只能在函数组件的最外层或自定义Hook中使用,不可嵌套在条件/循环中;
  2. 仅React函数调用‌:只能在React组件函数或自定义Hook中调用;
  3. 命名规范‌:自定义Hook必须以use开头(如useToggle)。

八、如何阻止组件渲染?

在组件中返回null,不会触发生命周期方法(如componentDidUpdate):

function Banner({ shouldShow }) {
  if (!shouldShow) return null; // 阻止渲染
  return <div>Special Offer!</div>;
}

九、如何设置组件默认的props值?

通过defaultProps静态属性:

function Button({ size }) { ... }
Button.defaultProps = {
  size: 'medium' // 未传递size时默认值为'medium'
};

十、React 元素(Element)和组件(Component)有什么区别?

  • 元素‌:普通JS对象,描述DOM节点及其属性,如<div className="test" />会被编译为:
{ type: 'div', props: { className: 'test' }, children: null }
  • 组件‌:接收props并返回元素的函数或类,可包含逻辑和状态管理。

react基础1

2025年4月7日 22:26

一、React Hooks的闭包陷阱是如何产生的?如何避免?

  • 产生原因‌:Hook依赖的state在闭包中被锁定,当异步操作使用旧闭包值时会出现数据不一致
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setTimeout(() => {
      // 始终输出初始值0(闭包陷阱)
      console.log(count); 
    }, 3000);
  }, []); // ❌ 空依赖数组
}
  • 解决方案‌:
  1. 使用函数式更新:setCount(c => c + 1)
  2. 通过useRef保持最新值引用
  3. 正确声明依赖数组

二、如何实现自定义Hook的TypeScript类型推断?

使用泛型与返回元组类型:

function useToggle<T extends boolean>(initialState: T): [T, () => void] {
  const [state, setState] = useState(initialState);
  const toggle = () => setState(!state);
  return [state, toggle];
}
// 使用时可自动推断类型
const [isOpen, toggleOpen] = useToggle(false); 

三、React.memo与useMemo的核心区别是什么?

React.memo useMemo
作用对象 组件级缓存 值/计算结果的缓存
触发条件 props变化时重新渲染 依赖数组变化时重新计算
典型场景 避免子组件不必要的渲染 昂贵计算/复杂对象创建的优化

四、如何用React Context实现细粒度性能优化?

  1. 拆分Context‌:将高频更新与低频更新的数据分离
  2. 使用Selector模式‌:
const UserContext = createContext();
const useUser = (selector) => {
  const context = useContext(UserContext);
  return selector(context);
}
// 组件中按需消费
const username = useUser(ctx => ctx.username);
  1. 配合useMemo防止不必要渲染

五、解释React并发模式(Concurrent Mode)的三个核心API

  1. startTransition‌:标记非紧急状态更新
jsxCopy Code
import { startTransition } from 'react';
// 用户输入立即响应,搜索结果延迟更新
startTransition(() => setSearchQuery(input));
  1. useDeferredValue‌:获取延迟版本的值
  2. Suspense‌:配合lazy加载组件时展示fallback

六、React服务端渲染(SSR)的hydration过程可能遇到什么问题?如何解决?

问题‌:

  • 客户端与服务端初始渲染不一致导致hydration失败
  • 第三方库依赖window对象引发SSR报错

解决方案‌:

  1. 使用useEffect/componentDidMount隔离浏览器API调用
  2. 服务端渲染时通过ReactDOMServer.renderToString生成静态HTML
  3. 客户端使用ReactDOM.hydrateRoot进行注水
  4. 检查data-reactroot属性是否一致

七、设计高阶组件时如何处理ref透传问题?

使用forwardRef与Ref转发:

const withLogger = (WrappedComponent) => {
  return React.forwardRef((props, ref) => {
    useEffect(() => {
      console.log('Component mounted');
    }, []);
    
    return <WrappedComponent {...props} ref={ref} />;
  });
}

八、React错误边界的实现原理与限制

原理‌:
通过类组件的static getDerivedStateFromError()componentDidCatch()捕获子组件树错误

限制‌:

  • 无法捕获以下错误:

    1. 事件处理器中的错误
    2. 异步代码(setTimeout、请求回调)
    3. 服务端渲染错误
    4. 错误边界组件自身的错误

九、如何用React实现双向数据绑定?

方案‌:
组合使用valueonChange,支持自定义表单控件:

function useModel(initialValue) {
  const [value, setValue] = useState(initialValue);
  return {
    value,
    onChange: e => setValue(e.target.value),
    // 支持非DOM场景
    setValue 
  };
}
// 使用
const inputProps = useModel('');
return <input {...inputProps} />;

十、React性能优化中如何正确使用useCallback?

黄金法则‌:

  1. 必要性检查‌:仅在以下场景使用:

    • 作为其他Hook的依赖项
    • 作为memoized组件的prop
  2. 稳定依赖‌:确保依赖数组完整且变化合理

  3. 反模式‌:避免在渲染层直接创建函数

// ✅ 正确用法
const handleClick = useCallback(() => {
  // 依赖state时需要声明依赖数组
}, [dep1, dep2]);

// ❌ 不必要的useCallback
const unstableFunc = useCallback(() => {}, []);

Webpack 在前端工程化中的核心应用解析-构建老大

作者 鱼樱前端
2025年4月7日 21:48

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~~~坚持自己观点

写点笔记写点生活~写点经验。 image.png

一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Webpack作为前端工程化构建工具基础重中之重,这个一定要清楚知道,vite,rollup,等基本上就是通的学会学懂~

一、Webpack 核心架构优势

  1. 模块化深度支持:原生支持 CommonJS/AMD/ESM 混合模块体系
  2. 生态完整性:超过 2500 个官方/社区 loader 和 plugin
  3. 渐进式编译:支持增量编译与持久化缓存(v5+)
  4. 开发体验优化:Hot Module Replacement 热更新机制

二、核心配置与工程化应用场景

1. 基础配置体系

 
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',        // 多入口支持 { app: '', admin: '' }
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    publicPath: '/'
  },
  module: {
    rules: [                     // 模块处理规则
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' })
  ]
};

2. 核心配置维度解析

入口优化(Entry Optimization)

 
// 动态入口配置
function dynamicEntries() {
  return glob.sync('./src/pages/**/index.js').reduce((entries, path) => {
    const name = path.split('/')[2];
    entries[name] = path;
    return entries;
  }, {});
}

module.exports = {
  entry: dynamicEntries()
};

模块解析策略(Resolution)

 
resolve: {
  extensions: ['.js', '.jsx', '.vue', '.json'], // 自动扩展后缀
  alias: {
    '@': path.resolve(__dirname, 'src/'),      // 路径别名
    'react-dom': '@hot-loader/react-dom'       // HMR优化
  },
  modules: [
    'node_modules',
    path.resolve(__dirname, 'shared')           // 自定义模块目录
  ]
}

高级优化配置(Optimization)

 
optimization: {
  splitChunks: {
    chunks: 'all',                            // 代码分割策略
    cacheGroups: {
      vendors: {
        test: /[\/]node_modules[\/]/,
        priority: -10
      }
    }
  },
  runtimeChunk: 'single',                     // 运行时文件分离
  minimizer: [
    new TerserPlugin({ parallel: true }),      // 并行压缩
    new CssMinimizerPlugin()                   // CSS压缩
  ]
}

三、Vue工程化最佳实践

1. 完整Vue项目配置

 
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  module: {
    rules: [
      {
        test: /.vue$/,
        use: 'vue-loader'                    // SFC处理器
      },
      {
        test: /.css$/,
        use: [
          'vue-style-loader',               // 服务端渲染友好
          'css-loader',
          'postcss-loader'                   // 自动添加前缀
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),                    // Vue必需插件
    new DefinePlugin({
      __VUE_OPTIONS_API__: true,             // 兼容选项式API
      __VUE_PROD_DEVTOOLS__: false           // 生产环境关闭devtools
    })
  ]
};

2. 微前端场景深度集成

 
// 主应用配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@http://cdn.com/app1/remoteEntry.js'
      },
      shared: {
        vue: { singleton: true },            // 单例模式共享Vue
        'vue-router': { singleton: true }
      }
    })
  ]
};

// 子应用配置
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Widget': './src/components/Widget.vue'
  },
  shared: require('./package.json').dependencies
});

四、React工程化最佳实践

1. 现代React项目配置

 
module.exports = {
  module: {
    rules: [
      {
        test: /.(js|jsx|ts|tsx)$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-react', { runtime: 'automatic' }], // 新版JSX转换
              '@babel/preset-typescript'
            ]
          }
        }
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      process: 'process/browser'            // 解决process未定义
    })
  ]
};

2. 服务端渲染优化方案

 
// 客户端配置
module.exports = {
  entry: {
    main: './src/client.js',
    hydrate: './src/hydrate.js'            // 客户端激活入口
  },
  output: {
    publicPath: 'http://cdn.example.com/', // CDN路径
    chunkLoadingGlobal: 'webpackJsonp'      // 避免多实例冲突
  }
};

// 服务端配置
module.exports = {
  target: 'node',
  output: {
    libraryTarget: 'commonjs2'             // Node兼容模块格式
  },
  externals: [nodeExternals()]             // 排除node_modules
};

五、性能优化关键路径

1. 构建速度优化

 
// 缓存策略(v5+)
module.exports = {
  cache: {
    type: 'filesystem',                   // 持久化缓存
    buildDependencies: {
      config: [__filename]               // 配置文件变更时失效
    }
  },
  snapshot: {
    managedPaths: [path.resolve('node_modules')] // 忽略node_modules快照
  }
};

// 多进程处理
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: require('os').cpus().length - 1
      })
    ]
  }
};

2. 输出质量优化

 
// Tree Shaking深度配置
module.exports = {
  optimization: {
    usedExports: true,                   // 标记未使用代码
    concatenateModules: true,            // 作用域提升
    sideEffects: true                    // 开启package.json的sideEffects
  }
};

// 按需加载配置
import(/* webpackPreload: true */ './ChartingLibrary'); // 预加载提示

六、企业级工程方案

1. 多环境配置体系

 
// webpack.base.js
module.exports = {
  // 公共配置
};

// webpack.dev.js
const { merge } = require('webpack-merge');
module.exports = merge(baseConfig, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    hot: true,
    historyApiFallback: true
  }
});

// webpack.prod.js
module.exports = merge(baseConfig, {
  mode: 'production',
  stats: 'errors-only',
  performance: {
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
});

2. 安全审计方案

 
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');

module.exports = {
  output: {
    crossOriginLoading: 'anonymous',       // CORS配置
  },
  plugins: [
    new SubresourceIntegrityPlugin({       // SRI校验
      hashFuncNames: ['sha384']
    }),
    new WebpackBundleAnalyzer({            // 包分析
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

七、未来演进方向

  1. 模块联邦扩展:实现跨应用模块实时共享
  2. 构建时编译:集成SWC/Rust工具链加速构建
  3. WebAssembly深度集成.wasm模块直接导入支持
  4. 生态融合:与Vite协同开发模式(Vite使用Webpack作为生产打包器)

架构选型建议:对于大型企业级应用建议采用Webpack作为核心构建工具,结合Module Federation实现微前端架构。对于新兴项目可考虑Vite开发环境 + Webpack生产构建的组合方案。

Vite 工程化深度解析与最佳实践

作者 鱼樱前端
2025年4月7日 21:47

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~~~坚持自己观点

一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一、Vite 核心架构优势

  1. ESM 原生支持:基于浏览器原生ES Modules的按需编译
  2. 极速冷启动:依赖预构建 + 模块缓存机制(node_modules/.vite)
  3. 双引擎架构:开发环境使用ESBuild(Go),生产环境用Rollup(JS)
  4. 闪电HMR:毫秒级热更新(平均<100ms)
  5. 未来标准支持:内置TypeScript/JSX/CSS Modules等处理

二、核心配置与工程化应用场景

1. Vite 6.0 最新工程化配置模板(2024官方推荐)

 
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  // 基础配置
  base: '/project/',
  root: process.cwd(),
  
  // 解析策略
  resolve: {
    alias: {
      '@': '/src',
      '#': '/types'
    },
    conditions: ['import', 'module', 'browser'],
    mainFields: ['module', 'jsnext:main', 'jsnext']
  },

  // 插件体系
  plugins: [
    vue({
      script: {
        defineModel: true, // 启用 Vue 3.4+ 新版 defineModel
        propsDestructure: true // 支持 props 解构
      }
    }),
    visualizer({
      template: 'treemap', // 新版可视化模式
      gzipSize: true,
      brotliSize: true
    })
  ],

  // 开发服务器
  server: {
    port: 5173,
    strictPort: true, // 禁止自动端口递增
    open: '/', // 自动打开浏览器
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ''),
        configure: (proxy) => {
          proxy.on('error', (err) => {
            console.log('[PROXY ERROR]', err)
          })
        }
      }
    }
  },

  // 构建配置
  build: {
    target: 'es2022',
    cssTarget: 'chrome118',
    minify: 'terser', // 推荐使用 esbuild(速度更快)
    terserOptions: {
      format: {
        comments: false
      }
    },
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          if (id.includes('node_modules')) {
            return 'vendor'
          }
        },
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash][extname]'
      }
    },
    reportCompressedSize: false // 禁用压缩尺寸报告
  },

  // 实验性特性(Vite 6 新增)
  experimental: {
    importGlobRestoreExtension: true, // 恢复导入时的扩展名
    hmrPartialAccept: true // 支持 Vue/React 的局部 HMR 接受
  },

  // 依赖优化
  optimizeDeps: {
    include: ['vue', 'vue-router'],
    esbuildOptions: {
      treeShaking: true,
      legalComments: 'none'
    }
  },

  // CSS 处理(Vite 6 强化配置)
  css: {
    transformer: 'lightningcss', // 默认使用 lightningcss
    lightningcss: {
      drafts: {
        nesting: true, // 启用 CSS 嵌套
        customMedia: true
      }
    },
    devSourcemap: true // 开发环境生成 sourcemap
  }
})

关键更新说明(Vite 6.0+)

  1. 闪电CSS默认启用

     
    css: {
      transformer: 'lightningcss' // 替代 PostCSS 成为默认处理器
    }
    
  2. 新版模块预构建

     
    optimizeDeps: {
      disabled: false, // 默认启用预构建
      needsInterop: [] // 手动指定需要转换的 CJS 模块
    }
    
  3. SSR 增强配置

     
    ssr: {
      format: 'esm', // 默认 ESM 格式
      target: 'node', // 自动识别 Node 环境
      noExternal: ['@vue/reactivity'] // 强制打包指定依赖
    }
    
  4. 实验性 Web 组件支持

     
    experimental: {
      webComponents: true // 启用 web components 支持
    }
    
  5. 构建缓存策略优化

     
    cacheDir: './.vite', // 默认缓存目录
    build: {
      cache: true // 默认启用构建缓存
    }
    

最新插件生态推荐

 
// 推荐插件列表
import legacy from '@vitejs/plugin-legacy' // 传统浏览器支持
import checker from 'vite-plugin-checker' // 类型检查
import progress from 'vite-plugin-progress' // 构建进度条

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11']
    }),
    checker({
      typescript: true,
      vueTsc: true
    }),
    progress()
  ]
})

性能优化配置扩展

 
// 高级优化配置
build: {
  cssMinify: 'lightningcss', // CSS 压缩引擎
  modulePreload: {
    polyfill: false // 现代浏览器无需 polyfill
  },
  dynamicImportVarsOptions: {
    warnOnError: true
  }
},
experimental: {
  buildAdvanced: {
    splitCss: true, // CSS 代码分割优化
    preloadStrategy: 'async' // 预加载策略
  }
}

企业级项目结构建议

 
├── .vite/                # 缓存目录
├── public/               # 纯静态资源
├── src/
│   ├── assets/           # 编译处理资源
│   ├── components/       # 通用组件
│   ├── composables/      # Vue 组合式 API
│   ├── layouts/          # 布局组件
│   ├── locales/          # 国际化文件
│   ├── middleware/       # 服务端中间件
│   ├── plugins/          # Vue 插件
│   ├── router/           # 路由配置
│   ├── stores/           # 状态管理
│   ├── styles/           # 全局样式
│   ├── types/            # 类型声明
│   ├── utils/            # 工具函数
│   └── views/            # 页面组件
├── .env                  # 环境变量
└── index.html            # 入口 HTML

2. 核心配置维度解析

模块解析策略

 
{
  resolve: {
    dedupe: ['vue'], // 强制单例
    conditions: ['esnext'], // 优先使用ESM版本
    mainFields: ['esnext', 'module', 'jsnext:main', 'main']
  }
}

依赖预构建优化

 
{
  optimizeDeps: {
    include: ['lodash-es', 'axios'],
    exclude: ['vue-demi'],
    esbuildOptions: {
      plugins: [
        esbuildCommonjs(['@vue/composition-api']) // CJS转ESM
      ]
    }
  }
}

高级构建配置

 
{
  build: {
    minify: 'terser', // 可选'esbuild'(更快但压缩率低)
    terserOptions: {
      compress: {
        drop_console: true
      }
    },
    sourcemap: 'hidden', // 生成但不关联sourcemap
    reportCompressedSize: false // 关闭gzip大小报告
  }
}

三、Vue工程化最佳实践

1. 企业级Vue3项目配置

 
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true // 启用实验性defineModel
      }
    }),
    Components({
      dts: 'types/components.d.ts', // 组件自动导入类型
      resolvers: [ElementPlusResolver()]
    }),
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'types/auto-imports.d.ts'
    })
  ],
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`
      }
    }
  }
})

2. 微前端深度集成

 
// 主应用配置
{
  plugins: [
    federation({
      name: 'host-app',
      remotes: {
        remote_app: 'http://localhost:5001/assets/remoteEntry.js'
      },
      shared: ['vue', 'pinia', 'vue-router']
    })
  ]
}

// 子应用配置
{
  build: {
    lib: {
      entry: 'src/entry.ts',
      formats: ['es'],
      fileName: 'remoteEntry'
    },
    rollupOptions: {
      external: ['vue', 'pinia'],
      output: { globals: { vue: 'Vue' } }
    }
  }
}

四、React工程化最佳实践

1. 现代React18项目配置

 
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react({
      jsxImportSource: '@emotion/react', // 支持CSS-in-JS
      babel: {
        plugins: ['@emotion/babel-plugin'],
        parserOpts: { plugins: ['decorators-legacy'] }
      }
    })
  ],
  esbuild: {
    logOverride: { 'this-is-undefined-in-esm': 'silent' }
  }
})

2. 服务端渲染方案

 
// SSR专用配置
{
  build: {
    ssr: true,
    rollupOptions: {
      input: 'src/entry-server.tsx',
      output: {
        dir: 'dist/server',
        format: 'cjs'
      }
    }
  },
  ssr: {
    noExternal: ['react-router-dom'] // 强制打包SSR依赖
  }
}

五、性能优化关键路径

1. 开发体验优化

 
{
  server: {
    warmup: {
      clientFiles: ['./src/main.ts', './src/app.css'] // 预编译关键文件
    }
  },
  preview: {
    open: 'chrome' // 自动打开指定浏览器
  }
}

2. 构建输出优化

 
{
  build: {
    assetsInlineLimit: 4096, // 小于4KB资源转base64
    chunkSizeWarningLimit: 1500, // 调整大包警告阈值
    cssCodeSplit: true // CSS代码分割
  }
}

3. 高级缓存策略

 
{
  cacheDir: '.vite_cache', // 自定义缓存目录
  optimizeDeps: {
    force: process.env.NODE_ENV === 'development' // 开发模式强制预构建
  }
}

六、企业级工程方案

1. Monorepo架构支持

 
// 子包配置(packages/app/vite.config.js)
export default defineConfig({
  root: path.resolve(__dirname, 'src'),
  build: {
    outDir: path.resolve(__dirname, '../../dist/app')
  },
  resolve: {
    alias: {
      '@shared': path.resolve(__dirname, '../../packages/shared')
    }
  }
})

2. 全链路监控体系

 
import { sentryVitePlugin } from "@sentry/vite-plugin"

{
  plugins: [
    sentryVitePlugin({
      authToken: process.env.SENTRY_AUTH_TOKEN,
      org: "my-org",
      project: "my-project",
      sourcemaps: {
        filesToDeleteAfterUpload: 'dist/**/*.map'
      }
    })
  ]
}

七、Vite5 最新特性实践

1. 闪电CSS引擎

 
{
  css: {
    transformer: 'lightningcss', // 替换PostCSS
    lightningcss: {
      drafts: {
        nesting: true // 启用CSS嵌套
      }
    }
  }
}

2. 内置WebSocket优化

 
{
  server: {
    hmr: {
      protocol: 'ws',
      host: 'localhost',
      port: 24678,
      overlay: false // 禁用错误遮罩层
    }
  }
}

3. 实验性特性

 
{
  experimental: {
    renderBuiltUrl(filename, { hostType }) {
      if (hostType === 'js') {
        return { runtime: `window.assetPath(${JSON.stringify(filename)})` }
      }
    }
  }
}

八、最佳实践提示

  1. 项目结构规范

     
    ├── src
    │   ├── assets        # 静态资源
    │   ├── components    # 公共组件
    │   ├── composables   # Vue组合式API
    │   ├── layouts       # 布局组件
    │   ├── router        # 路由配置
    │   ├── stores        # 状态管理
    │   ├── styles        # 全局样式
    │   ├── types         # 类型声明
    │   └── views         # 页面组件
    
  2. 插件推荐清单

    • vite-plugin-inspect 分析中间产物
    • vite-plugin-checker 并行类型检查
    • vite-plugin-pwa PWA支持
    • vite-plugin-mock 模拟数据
  3. 环境策略

     
    // .env.production
    VITE_API_BASE = https://api.example.com
    NODE_ENV = production
    
    // 使用方式
    console.log(import.meta.env.VITE_API_BASE)
    

架构选型建议:新项目首选Vite作为全流程构建工具,大型项目可采用Vite(开发)+ Rollup(库打包)组合。建议保持Vite版本在4.3+以获取最佳性能体验,生产部署配合CDN和HTTP/2 Server Push实现极致加载速度。

多人协同编辑算法 —— CRDT 算法 🐂🐂🐂

作者 Moment
2025年4月7日 21:39

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

什么是 CRDT

无冲突复制数据类型(CRDT,Conflict-free Replicated Data Types)是一类在分布式系统中用于数据复制的数据结构,旨在解决多副本并发更新时的数据一致性问题。CRDT 允许各个副本独立且并发地进行更新,而无需协调,且能够在最终自动解决可能出现的不一致性。

CRDT 的关键特性主要有以下三个方面:

  1. 独立更新: 每个副本可以独立地进行更新,无需与其他副本进行通信。

  2. 自动合并: 当副本之间交换数据时,CRDT 会自动合并更新,确保所有副本最终达到一致状态。

  3. 最终一致性: 尽管副本可能在某些时刻处于不同状态,但通过合并操作,所有副本最终会收敛到相同的状态。

CRDT 的种类

CRDT 有两种方法,都可以提供强最终一致性:基于操作的 CRDT 和基于状态的 CRDT。

基于操作的 CRDT(CmRDT)

基于操作的 CRDT(也称为交换性复制数据类型,CmRDT)是一类通过传输更新操作来同步副本的 CRDT。在 CmRDT 中,每个副本只发送更新操作,而不是完整的状态。例如,操作可以是"+10"或"-20",它们表示对某个值的增减。副本接收到这些操作后,会在本地应用这些更新。

操作是可交换的,这意味着操作的顺序不影响最终结果。也就是说,即使操作以不同的顺序应用,最终的结果也会是一样的。然而,这些操作不一定是幂等的,即重复应用相同操作可能会产生不同的结果。

由于操作是以独立的方式广播的,通信基础设施必须保证所有操作都被传输到所有副本,而且操作不会丢失或重复。在此过程中,操作的顺序是灵活的,可以按照任何顺序传输。

纯基于操作的 CRDT(Pure CmRDT) 是基于操作的 CRDT 的一个变种,它通过减少所需的元数据大小来优化性能。

G-Counter

G-Counter 用于实现分布式环境中的计数器功能,由多个计数器组成的数据结构,每个副本都维护自己的计数器。每当副本需要增加计数时,它只会在自己的计数器上增加,而不会减少或修改其他副本的计数器。当需要获取计数时,副本会将所有计数器的值累加起来,以获得全局的计数结果。G-Counter 是一个只增长的计数器,它满足如下的性质:

  1. 每个副本的计数器值只增加,不会减少。

  2. 副本之间的计数器值可以独立增长,不会发生冲突或合并操作。

PN-Counter

PN-Counter 是一种基于 CRDT 的数据类型,用于实现分布式环境中的计数器功能。由两个 G-Counter 组成的数据结构,分别用于记录正数和负数的计数。每个副本都维护自己的两个计数器,分别用于增加正数计数和增加负数计数。当需要获取计数时,副本会将正数计数器的值减去负数计数器的值,以获得全局的计数结果。PN-Counter 具有以下性质:

  1. 每个副本的正数计数器和负数计数器值只增加,不会减少。

  2. 副本之间的计数器值可以独立增长,不会发生冲突或合并操作。

  3. 全局计数结果是正数计数减去负数计数的差值。

Sequence CRDT

Sequence CRDT 用于实现分布式环境中的有序序列功能,旨在解决在并发环境中对有序序列进行并发操作时可能出现的冲突问题。它允许并发操作在不同副本之间交换和合并,以达到最终一致性的状态。Sequence CRDT 的实现方式有多种,其中一种常见的实现是基于标识符(Identifier)的方式。每个操作都被赋予唯一的标识符,用于标识操作的顺序。常见的操作包括插入元素、删除元素和移动元素。通过使用标识符和一致的合并策略,Sequence CRDT 可以实现在分布式环境中对有序序列进行并发操作的正确合并。具体的合并策略可以根据应用需求和具体实现进行定制。Sequence CRDT 具有以下特性:

  1. 并发操作可以独立地在不同副本上执行,不会发生冲突。

  2. 合并操作时,可以根据标识符和合并策略将并发操作正确地合并到最终结果中。

Sequence CRDT 可以应用于许多场景,如协同编辑、版本控制系统、聊天应用等,其中有序的操作是必要的。它提供了在分布式环境中实现有序序列的能力,并保持最终一致性。

基于状态的 CRDT(CvRDT)

与基于操作的 CRDT(CmRDT)不同,基于状态的 CRDT(也称为收敛复制数据类型,CvRDT)通过将完整的本地状态发送到其他副本来进行状态传播。在 CvRDT 中,副本接收到完整的状态并将其与自身的状态合并。合并函数必须满足可交换性结合性幂等性,确保副本之间的合并结果是相同的。

这意味着合并操作的顺序不影响最终结果,并且即使多次合并相同的状态,结果也不会发生变化。所有副本的状态都可以通过合并来收敛到同一个最终状态。为了确保一致性,更新函数必须遵循一个偏序规则,使得每次合并都能够单调地增加内部状态。

Delta 状态 CRDT 是基于状态的 CRDT 的一种优化版本。在 Delta CRDT 中,仅传播最近对状态进行的更改(即"delta"),而不是将整个状态传输到其他副本。这减少了每次更新的网络开销,并提高了效率。只有当某个副本的状态发生变化时,才会将该变化广播给其他副本,从而避免了大量不必要的数据传输。

G-Set

G-Set 是一种基于 CRDT 的数据类型,用于实现分布式环境中的集合功能,G-Set 是一个只增长的集合,每个副本都维护自己的本地集合。当需要添加元素时,副本只会在自己的集合中添加元素,而不会删除或修改其他副本的集合。G-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地增加元素。

  2. 全局集合是所有副本的集合的并集,即包含所有副本中添加的元素。

由于 G-Set 是只增长的集合,它满足最终一致性和合并性质。每个副本的本地集合可以独立地增长,不会发生冲突或合并操作。当需要获取全局集合时,可以简单地将所有副本的集合合并。G-Set 适用于需要在分布式环境中维护集合,并且可以实现高可用性和最终一致性的场景。它常用于记录一组唯一的元素,而不需要删除或修改元素

2P-Set

2P-Set 用于实现分布式环境中的集合功能,维护两个集合:一个"添加集合"和一个"移除集合"。每个元素在添加集合中只能添加一次,在移除集合中只能移除一次。这样,2P-Set 可以实现添加和移除元素的操作,并且确保元素不会重复添加或移除。2P-Set 的操作包括:

  1. 添加元素:将元素添加到添加集合中。

  2. 移除元素:将元素从添加集合中移除,并将其添加到移除集合中。

2P-Set 的特性包括:

  1. 每个副本维护自己的本地添加集合和移除集合,可以独立地进行添加和移除操作。

  2. 全局集合是添加集合减去移除集合的结果。

当需要获取全局集合时,副本将所有副本的添加集合和移除集合合并,并计算添加集合减去移除集合的结果,得到最终的全局集合。2P-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录添加和移除操作,并且不希望元素重复添加或移除的场景。

LWW-Element-Set

LWW-Element-Set 用于实现分布式环境中的集合功能,维护一个集合,其中每个元素都与一个时间戳相关联。时间戳可以是递增的整数,逻辑时钟,或其他可比较的时间表示。每当需要添加或移除元素时,副本会将元素与当前时间戳关联,并将操作应用到本地集合。LWW-Element-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地添加或移除元素。

  2. 全局集合是根据时间戳确定的最新操作的结果,即最后的写操作胜出。

当需要获取全局集合时,副本将所有副本的集合合并,并根据时间戳选择最新的操作。如果存在多个副本对同一个元素进行了不同的操作,那么具有较新时间戳的操作将覆盖较旧时间戳的操作。LWW-Element-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录元素的添加和移除,并以最后写操作为准的场景。然而,由于最后写操作胜出的特性,可能会导致某些操作的冲突或覆盖

OR-Set

OR-Set 用于实现分布式环境中的集合功能,维护一个集合,其中每个元素都与一个标识符相关联。当需要添加元素时,副本会为元素生成一个唯一的标识符,并将其添加到本地集合中。当需要移除元素时,副本会为要移除的元素生成一个移除标记,并将其关联到原始元素的标识符上。OR-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地添加和移除元素。

  2. 全局集合是所有副本的集合的并集,其中移除标记会覆盖对应的元素。

当需要获取全局集合时,副本将所有副本的集合合并,并根据标识符和移除标记进行操作。如果一个元素的标识符存在于集合中,但它的移除标记也存在,则该元素被视为已移除。这样,移除操作具有优先级高于添加操作的效果。OR-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录元素的添加和移除,并且允许移除操作覆盖添加操作的场景。

CmRDTs 和 CvRDTs

相比于 CvRDTs,CmRDTs 在副本之间传输操作的协议上有更多要求,但当事务数量相对于内部状态的大小较小时,它们使用的带宽较少。然而,由于 CvRDT 的合并函数是可结合的,与某个副本的状态进行合并会包含该副本的所有先前更新。在减少网络使用和处理拓扑变化方面,使用 Gossip 协议可以很好地传播 CvRDT 状态到其他副本。

CRDT 的数学基础

CRDT 的核心在于其合并操作必须满足一组特定的数学性质,这些性质保证了在分布式系统中数据最终能够达到一致。合并操作(通常表示为 ∨)必须满足以下三个关键性质:

1. 交换律(Commutativity)

合并操作的顺序不影响最终结果:

[ A \vee B = B \vee A ]

这意味着无论是节点 A 先将数据同步给节点 B,还是节点 B 先将数据同步给节点 A,最终的结果都是一样的。这个性质对于分布式系统特别重要,因为在实际环境中,我们无法保证数据同步的顺序。

示例:

节点1的状态: {a, b}
节点2的状态: {b, c}
合并结果: {a, b, c}  // 无论是12还是21的同步顺序,结果都相同

2. 结合律(Associativity)

多个合并操作的顺序不影响最终结果:

[ (A \vee B) \vee C = A \vee (B \vee C) ]

这个性质确保了在有多个节点时,无论按什么顺序进行合并,最终结果都是一致的。这对于大规模分布式系统尤为重要,因为数据同步可能涉及多个节点的链式传递。

示例:

节点1的状态: {a, b}
节点2的状态: {b, c}
节点3的状态: {c, d}

(节点1 ∨ 节点2) ∨ 节点3 = {a, b, c} ∨ {c, d} = {a, b, c, d}
节点1 ∨ (节点2 ∨ 节点3) = {a, b} ∨ {b, c, d} = {a, b, c, d}

3. 幂等律(Idempotency)

重复合并不会改变结果:

[ A \vee A = A ]

这个性质保证了即使同一个更新被应用多次(例如由于网络问题导致的重复传输),也不会影响最终状态。这对于构建容错的分布式系统至关重要。

示例:

状态A: {a, b, c}
AA = {a, b, c}  // 重复合并不会产生新的结果

实际意义

这些数学性质的重要性体现在:

  1. 网络分区容忍: 由于交换律和结合律的存在,系统可以在网络分区的情况下继续工作,当连接恢复后可以正确合并数据。

  2. 最终一致性保证: 这些性质确保了无论数据同步的顺序如何,所有副本最终都会收敛到相同的状态。

  3. 去中心化: 不需要中央协调器来处理并发更新,每个节点都可以独立处理更新并最终达到一致。

  4. 容错性: 幂等性确保了系统能够优雅地处理重复的消息,这在不可靠的网络环境中特别重要。

在实际应用中,这些性质使得 CRDT 特别适合构建:

  • 协同编辑系统
  • 分布式数据库
  • 多设备数据同步
  • 离线优先的应用

通过确保这些数学性质,CRDT 能够在不需要复杂的协调机制的情况下,保证分布式系统中数据的最终一致性。

CRDT 是如何处理冲突的

下图描述了 Yjs 中处理冲突的算法模型,它是一个支持点对点传输的冲突处理模型。

20250407170244

首先我们先来解释一下图中的元素:

  1. E(1,0):表示用户 1 在节点 A 和 B 之间插入了数据项。

  2. D(0,1):表示用户 0 在节点 B 和 C 之间插入了数据项。

  3. C(0,0):表示用户 0 在节点 A 和 B 之间插入了数据项。

图示的操作顺序:

  1. 用户 0 插入了 C(0,0) 在节点 A 和 B 之间。

  2. 用户 0 在节点 B 和 C 之间插入了 D(0,1)。

  3. 用户 1 插入了 E(1,0) 在节点 A 和 B 之间。

当两个操作发生并发冲突(例如 C(0,0) 和 E(1,0) 都涉及节点 A 和 B 之间的插入),Yjs 会基于操作的用户标识符来决定哪一个操作先应用。

在这个例子中,用户 1 的标识符(1)大于用户 0 的标识符(0),因此生成的最终文档顺序是 A C D E B

CRDT 机制能够避免传统操作转发(OT)所面临的冲突问题,同时保证最终一致性,原因在于其设计采用了冲突自由的合并规则,而不依赖于复杂的操作变换和中央协调。

在 OT 中,当多个用户并发地对同一数据进行操作时,系统需要通过操作转发和变换来确保操作顺序的一致性。这通常涉及复杂的变换逻辑,例如在两个用户同时修改相同数据位置时,OT 会通过变换算法调整其中一个操作的位置或内容,以确保最终结果一致。尽管 OT 可以解决许多并发冲突,但这种变换机制本身具有高复杂性,特别是在多个用户同时进行操作时,操作的变换和冲突解决可能导致性能瓶颈、维护困难,以及在极端情况下可能产生不一致的结果。

与此不同,CRDT 通过设计内建的合并规则来避免这些问题。每个 CRDT 数据结构都确保其操作是幂等、交换性强且结合性好的,这意味着无论操作顺序如何或是否发生并发操作,所有副本都能够自动且无冲突地合并,最终收敛到一致的状态。CRDT 不依赖于操作的顺序或中央协调,而是依靠每个操作的唯一标识符和局部合并规则来直接解决并发冲突,从而显著减少了在处理冲突时的计算复杂度。

此外,CRDT 的这一机制使得它天然适合高可用性和容错性要求较高的分布式系统,在面对网络分区、节点故障等场景时,系统依然能够继续操作并保证数据一致性。因此,CRDT 更加简洁、易于扩展,并能够在没有显式操作转发和变换的情况下,确保最终一致性,从根本上避免了 OT 中因操作顺序和变换导致的复杂性和潜在冲突。

CRDT 如何解决脏路径问题

在分布式系统中,脏路径(Dirty Path)问题通常出现在多个副本之间进行并发操作时,导致副本之间的数据状态不一致。由于不同副本的操作可能由于网络延迟、分区或同步问题而不同步,这使得系统中可能出现不一致的数据状态。传统的分布式系统通常依赖中心化的协调机制来同步数据,但这也容易引发性能瓶颈和复杂的冲突解决问题。CRDT(冲突自由复制数据类型)通过去中心化和无冲突的操作设计,避免了脏路径问题,确保多个副本能够在并发操作后最终收敛到一致的状态。

以下是 CRDT 如何通过一系列设计原则来解决脏路径问题的详细过程:

1. 唯一标识符与操作标记

CRDT 使用唯一标识符来区分每个操作,每个操作的标识符通常由 用户标识符(例如用户 ID)和 操作序列号(通常是时间戳或递增的操作编号)组成。唯一标识符保证了操作的顺序,即使这些操作在不同副本上并发发生。

操作标识符的作用:

  • 用户标识符(例如 AB):确保每个用户的操作是唯一的,防止不同用户的操作发生混淆。
  • 操作序列号(例如 01):确保同一用户的操作能够按序列号区分,确定操作的顺序。

这种设计避免了因操作没有明确顺序而产生的不一致或冲突,从而有效地避免了脏路径问题。

示例:

假设用户 A 在副本 1 上插入了一个字符 X,操作标识符为 A,0。与此同时,用户 B 在副本 2 上插入了字符 Y,操作标识符为 B,0。每个操作都带有唯一标识符,确保它们在后续合并时能够正确排序。

2. 并发操作的解决

在 CRDT 中,每个副本都能够独立进行操作,当多个副本发生并发操作时,CRDT 使用设计的 合并规则 来自动解决冲突,确保所有副本最终达到一致状态。

如何处理并发操作?

  • 当用户 A 在副本 1 上插入字符 X,并且用户 B 在副本 2 上插入字符 Y 时,两个操作会先在本地副本上执行,然后通过网络传播到其他副本。
  • CRDT 会通过 操作标识符 比较来确定哪个操作先执行。比如,比较 A,0B,0,标识符较小的操作会先应用。

例如,假设 A,0 小于 B,0,那么操作会按顺序执行,首先在副本 1 上插入 X,然后在副本 2 上插入 Y

3. 合并规则与最终一致性

CRDT 的设计关键在于 合并规则,即如何将并发操作合并为一致的状态。这些合并规则确保了即使副本之间的操作顺序不同,最终副本的数据会收敛到相同的状态。

合并规则保证一致性:

  1. 幂等性(Idempotence):一个操作可以多次应用,结果不会改变。如果某个操作被传送到一个副本多次,只会影响一次,避免重复操作带来的问题。
  2. 交换性(Commutativity):操作的顺序不影响最终结果。不同副本的操作可能发生顺序不同,但最终合并时,所有副本的数据状态将是一致的。
  3. 结合性(Associativity):多个操作的合并顺序不影响结果。即使合并操作的顺序不同,最终的合并结果相同。

这些规则使得 CRDT 在面对并发更新时,能够自动解决冲突并收敛到一致的状态。

举例说明:

假设两个用户并发进行插入操作,用户 A 在副本 1 中插入 X,而用户 B 在副本 2 中插入 Y。无论这两个操作的顺序如何,CRDT 会根据合并规则确定最终的顺序,并保证合并后的状态一致。即使两个副本的操作顺序不同,最终的结果将是文本 "XY""YX"(具体顺序依赖于标识符的比较)。

4. 双向链表在 CRDT 中的应用

在一些 CRDT 应用(例如文本编辑器)中,双向链表 被用来存储数据。双向链表的结构非常适合表示具有顺序关系的数据,并且支持高效的插入、删除和更新操作。

双向链表如何解决脏路径问题:

  • 插入和删除操作:当用户在文本中插入或删除字符时,CRDT 会将这些操作表示为双向链表的节点。每个节点都包含指向前一个和后一个节点的指针,使得操作能够在链表的任意位置进行。
  • 并发操作:当多个用户在不同副本上同时修改文本时,CRDT 会根据操作的唯一标识符(例如标识符的大小)来决定操作的顺序。例如,用户 A 在某位置插入字符,用户 B 在另一个位置插入字符,CRDT 会通过合并规则确保这两个操作按正确顺序合并,并更新链表。

通过这种方式,CRDT 可以处理并发插入、删除操作,避免因操作顺序不同而引发脏路径问题。

5. 最终一致性

CRDT 通过合并规则确保所有副本最终一致。即使操作在不同副本之间发生延迟或顺序不同,最终的合并结果会保证一致性。

如何确保最终一致性?

  • 去中心化:CRDT 不依赖中心化的协调,所有副本都能独立执行操作并进行合并。每个副本都维护自己的操作历史,并通过合并规则来自动解决冲突。
  • 同步与传播:每个副本定期与其他副本同步,传播其操作。即使某些副本的更新稍有延迟,最终每个副本的状态都会通过合并收敛到一致。

通过最终一致性,CRDT 确保即使在网络分区或节点故障的情况下,系统中的所有副本最终都会收敛到相同的数据状态,避免了脏路径问题。

6. 避免脏路径:总结

CRDT 解决脏路径问题的关键在于:

  1. 唯一标识符:每个操作都有唯一标识符,确保并发操作能够按照正确顺序合并。
  2. 去中心化合并:CRDT 不依赖中心节点,而是通过去中心化的方式进行合并,每个副本根据合并规则独立解决冲突。
  3. 合并规则的设计:CRDT 使用幂等性、交换性和结合性保证操作的合并无冲突,确保最终一致性。
  4. 双向链表:在存储数据时,双向链表能够高效支持插入和删除操作,并保证操作的顺序正确,同时避免复杂的全局排序。
  5. 最终一致性:CRDT 确保每个副本最终一致,不论操作顺序如何,最终所有副本都会达成一致,避免了因不同步或操作冲突带来的脏路径问题。

通过这些机制,CRDT 确保了分布式系统中的高可用性、容错性和一致性,避免了脏路径问题的出现,并且简化了分布式系统中并发操作的管理。

CRDT 如何解决 UNDO/REDO 问题

在分布式系统中,UNDOREDO 是常见的操作需求,尤其是在分布式应用(如分布式文本编辑器、协作平台等)中,这些操作通常需要确保数据的一致性和正确的操作回溯。然而,传统的事务日志和操作转发(OT)机制在处理这些操作时可能会遇到同步、顺序和冲突等问题。而 CRDT(冲突自由复制数据类型)通过其特有的设计原则,能够优雅地解决 UNDOREDO 问题,保证分布式系统中操作的回滚与重做能够在多个副本间一致地执行。

什么是 UNDOREDO

  • UNDO:是撤销上一步操作的功能,即恢复到操作前的状态。在分布式系统中,UNDO 通常需要回滚到某个特定的历史状态。
  • REDO:是重新执行撤销操作后的功能,将上一步撤销的操作重新应用于数据中。

在分布式系统中,UNDOREDO 需要跨多个副本同步,以保证每个副本中的历史操作可以一致地回滚或重做。此过程可能会受到以下问题的影响:

  • 并发冲突:不同副本上的操作顺序不同,可能会导致状态不一致,尤其是在操作顺序发生变化时。
  • 操作历史的同步:在没有中心化控制的情况下,操作历史的同步可能会变得非常复杂。
  • 最终一致性:确保在分布式环境中,UNDO 和 REDO 不会导致不同副本的数据不一致。

CRDT 如何解决 UNDOREDO 问题

CRDT 提供了一些特性,使其特别适合解决 UNDOREDO 问题,尤其是在分布式环境下。这些特性包括 冲突自由的操作合并幂等性交换性结合性、以及 最终一致性。通过这些特性,CRDT 可以处理操作回滚和重做时遇到的挑战。

1. 操作历史与逆向操作(Undo/Redo)

CRDT 中的每个操作(如插入、删除等)都有一个唯一标识符。通过设计合适的操作历史结构,CRDT 可以存储每个操作,并支持操作的回溯和重做。这对于分布式系统中的 UNDOREDO 操作至关重要。

操作的存储和标识:

  • 每个操作都有唯一标识符,通常由操作的用户 ID 和时间戳(或操作序列号)组成。
  • CRDT 通常维护一个操作的日志或历史记录,其中记录了所有历史操作以及它们的操作标识符。由于 CRDT 的操作是幂等的(即重复执行不改变结果),因此可以安全地记录和回滚这些操作。

操作回滚(UNDO):

  • UNDO 操作需要逆向地应用上一个操作。例如,如果用户插入了一个字符,UNDO 就需要撤销该插入操作。
  • 在 CRDT 中,通过 逆向操作 来回滚数据。例如,如果插入操作是通过一个 增量计数器(例如 PN-Counter)进行的,UNDO 操作会通过逆向操作递减计数器的值,从而撤销上一步的插入。

操作重做(REDO):

  • REDO 操作需要将之前撤销的操作重新应用。例如,如果用户撤销了插入字符 X,则 REDO 会重新执行插入字符 X 的操作。
  • 在 CRDT 中,REDO 操作是重新应用已撤销的操作。这些操作会根据其标识符再次插入或删除数据,并通过合并规则确保最终一致性。

2. 如何支持并发和冲突解决

在分布式系统中,UNDOREDO 操作通常是在多个副本之间执行的,可能会遇到并发冲突的问题。CRDT 的核心特性能够有效地解决并发冲突问题,从而确保 UNDOREDO 操作的一致性。

幂等性、交换性和结合性:

  • 幂等性:确保同一个操作多次应用不会改变最终的结果。例如,当执行 UNDO 时,即使该操作多次传递给不同副本,它的效果仍然是相同的。
  • 交换性:多个操作的顺序不会影响最终结果。即使在不同副本上执行 UNDOREDO 操作,操作的顺序不会影响最终的数据一致性。
  • 结合性:多个 UNDOREDO 操作的顺序不影响结果。无论如何组合多个操作,系统最终会达到一致状态。

这些特性使得 CRDT 在多个副本上执行 UNDOREDO 操作时,可以自动解决并发冲突,确保不同副本的数据始终一致。

解决并发冲突的方式:

  • 当多个用户在不同副本上进行并发操作(如同时执行插入、删除或撤销操作)时,CRDT 会根据每个操作的标识符(例如时间戳、序列号等)来确定它们的顺序。
  • 即使副本之间的操作顺序不同,CRDT 通过标识符确保每个操作按正确的顺序合并,从而保证 UNDOREDO 操作能够正确地同步到所有副本。

示例:

  • 假设两个用户 A 和 B 同时进行文本插入操作,在某个时刻用户 A 撤销了插入操作,而用户 B 在该位置再次插入了文本。CRDT 会根据操作的标识符来判断用户 A 的撤销操作和用户 B 的插入操作的执行顺序,保证最终文本的一致性。

3. 最终一致性与操作回溯

CRDT 的设计目标之一是 最终一致性。即使操作的执行顺序不同,所有副本最终都会达到一致的状态。对于 UNDOREDO 操作,CRDT 确保它们的执行不会破坏最终一致性。

确保一致性:

  • 合并操作:CRDT 保证所有副本都会根据合并规则最终收敛到一致的状态。无论是 UNDO 还是 REDO 操作,系统通过合并规则将操作结果应用到每个副本,最终所有副本的数据会一致。
  • 最终一致性:操作的回溯(如 UNDOREDO)不会导致系统中的副本进入不一致的状态,因为每个副本都独立地执行操作并与其他副本同步,最终收敛到一致的数据状态。

4. 双向链表的应用

在一些具体的 CRDT 实现中(例如分布式文本编辑器),使用 双向链表 来存储数据,这使得 UNDOREDO 操作更容易实现。

双向链表支持操作回溯:

  • 每个节点表示一个操作或数据项,操作的顺序通过前驱和后继指针进行连接。通过这个数据结构,撤销(UNDO)和重做(REDO)可以通过更新链表的指针来高效实现。
  • UNDO:通过回退指针来撤销最近的操作,将前驱指针指向当前操作的前一个节点。
  • REDO:通过更新指针来重做撤销的操作,恢复后继指向已撤销操作的下一个节点。

双向链表使得撤销和重做操作在数据结构中非常高效,并且能够根据唯一标识符和合并规则来正确解决冲突。

CRDT 通过以下几个关键特性解决了 UNDOREDO 问题:

  1. 唯一标识符:每个操作有唯一标识符,确保回溯和重做时能按正确顺序执行。
  2. 幂等性、交换性和结合性:这些特性确保了 UNDOREDO 操作的正确性,并且即使在并发操作的情况下,也能够保证一致性。
  3. 去中心化合并:每个副本独立处理 UNDOREDO,并通过合并规则确保最终一致性。
  4. 双向链表:为 UNDOREDO 提供高效的操作存储和回溯机制,特别适合用于文本编辑等场景。

通过这些机制,CRDT 在分布式环境下不仅保证了 UNDOREDO 的一致性,还有效解决了并发冲突和操作历史同步的问题。

CRDT 解决并发冲突

接下来我们将以图片设置 align 属性为例介绍,首先看看 CRDT 如何描述对象属性及属性修改:

20250407204143

左边是图片数据模型,右边是模拟 CRDT 对应的数据结构,图片对象中的每一个字段都使用结构对象去描述内容及内容的修改,这里以 align 字段的代表看它的表达

操作 1️⃣:

20250407204221

左边是图片数据模型,右边是模拟 CRDT 对应的数据结构,图片对象中的每一个字段都使用结构对象去描述内容及内容的修改,这里以 align 字段的代表看它的表达

图中最上方的蓝色结构表示 align 属性的初始值为 "center",其对应的数据结构标识为 (140,20),表示该值由某个用户在某个时刻的操作产生。

随后,用户执行了操作 ①,将 align"center" 修改为 "left",从而生成了一个新的结构对象(图中橙色部分),其标识符为 (141,0)。这个新对象通过 left 指针指向其前一个状态 (140,20),表示该修改是基于 "center" 状态进行的。此时,Map 中的 align 字段被更新为指向这个新的对象。

⚠️ 值得注意的是:此结构中的 leftright 同时承担了两个不同含义——

  • 一方面,它们是链表结构中的指针字段,用于描述节点之间的连接关系;
  • 另一方面,align 的属性值也恰好是 "left""center""right" 之一。

为避免混淆,请理解:结构对象中间的那一块,才是真正表示属性值的内容,而两侧的 left / right 是链表的结构指针。比如在该示例中,中间的 "left" 是修改后的 align 值,而左侧的 left 指针连接了前一个状态 (140,20)

操作 2️⃣:

20250407204845

当然!以下是你后续“并发修改”部分的润色版本,紧接在“顺序修改”之后,风格统一,逻辑清晰,读起来也更专业:

与前面的顺序修改不同,在并发场景中,多个用户几乎同时基于相同的状态进行修改操作。此时,CRDT 会采用特定的合并策略来决定各个操作的插入顺序,从而确保所有副本最终达成一致。

如图所示,这一次有两个用户同时基于状态 (140,20)(即 align = "center")分别执行了修改操作:

  • 用户 A 将 align 改为 "left",生成结构对象 (141,0)

  • 用户 B 将 align 改为 "right",生成结构对象 (142,0)

由于这两个操作是并发的,它们都指向相同的前置节点 (140,20),即具有相同的“前提条件”。此时系统将根据每个操作的唯一标识符进行排序合并——在本例中,(141,0) 的优先级低于 (142,0),因此 "left" 会先插入链表,紧接着是 "right"

最终形成的双向链表结构如下:

center → leftright
         ↑      ↑
     (141,0) (142,0)

系统将 align 字段指向链表尾部的最新节点 (142,0),因此最终的属性值为 "right"

这种机制展现了 CRDT 在面对并发修改时的优势:无需冲突检测,也不丢失任一修改历史,并能通过一致的排序规则达成最终一致性。

下面看看两个用户并发的执行属性修改时产生的数据结构:

20250407205558

与前面的顺序操作不同,此处执行的操作 ① 和操作 ② 是并发修改:它们都是基于同一个前置状态,即 align = "center"(标识符为 (140,20))所发起的修改操作。

具体来说:

  • 操作 ① 将 align 修改为 "left",生成新结构对象 (141,0)

  • 操作 ② 将 align 修改为 "right",生成新结构对象 (142,0)

由于两个修改操作的基础状态相同,因此构成并发。在这种情况下,CRDT 会根据标识符的全局有序性来进行合并处理。

在本示例中,(141,0) 的标识符小于 (142,0),因此系统会按照如下顺序进行集成:

  1. 先插入 "left"(操作 ①)

  2. 再插入 "right"(操作 ②)

最终形成如下链表结构:

center → leftright
          ↑       ↑
      (141,0)  (142,0)

因此,最终 align 的属性值为 "right",即指向最新插入的节点 (142,0)

这一过程体现了 CRDT 对并发操作的自动合并能力:无需人工干预或冲突解决策略,仅通过标识符排序,就能实现一致性和可预期的合并结果。

顺序修改 vs 并发修改:对比总结

项目 顺序修改 并发修改
操作基础状态 每次操作都基于最新状态 多个操作基于相同的旧状态并发发生
是否存在冲突 无冲突,顺序明确 存在潜在冲突,需合并处理
合并方式 顺序串接,每个结构对象引用上一个 利用标识符排序合并,构建多分支链表结构
是否保留全部修改 ✅ 是,保留完整历史 ✅ 是,所有并发修改都会被表达
最终结果决定方式 最后一个操作决定当前值 标识符最大的修改赢得当前值归属
示例回顾 "center""left""right" "center"["left", "right"],最终为 "right"

在 CRDT 模型下,无论是顺序修改还是并发修改,都能通过结构化的数据表示 + 有序标识符来安全地整合操作,确保最终状态一致,并完整保留修改轨迹。这正是 CRDT 在协同编辑、离线同步等场景下强大而可靠的基础。

参考文章

总结

CRDT(无冲突复制数据类型)是一类用于分布式系统中的数据结构,它通过内建的幂等性、交换性和结合性操作,支持各副本在无协调情况下独立更新并自动合并,最终收敛为一致状态。它避免了传统并发控制中对冲突的显式处理,适用于离线编辑、多端同步、协同操作等高可用场景。通过唯一标识符和结构化合并策略,CRDT 能在面对并发修改、网络分区等挑战时保持数据一致性和操作完整性。

TinyPro 中后台管理系统使用指南——让页面搭建变得如此简单!

2025年4月7日 20:13

本文由体验技术团队Kagol原创。

TinyPro 是一个基于 TinyVue 打造的前后端分离的后台管理系统,支持在线配置菜单、路由、国际化,支持页签模式、多级菜单,支持丰富的模板类型,支持多种构建工具,功能强大、开箱即用。

1 项目优势

如果将开发 Web 应用比作建造房子,那么 TinyVue 组件库中的组件就是建造房子的砖、瓦、沙石等材料,你可以用 TinyVue 组件快速搭建前端项目。

而 TinyPro 则是用 TinyVue 组件库搭建起来的“样板房”,这是一个已经搭建好的完整的后台管理系统,包含前后端。

市面上有很多后台管理模板,为什么要选择 TinyPro 呢?

我总结了下,TinyPro 主要有以下优势:

  1. 上手成本低:一行命令即可创建一个后台管理系统
  2. 支持前后端:前端基于 Vue3 + TypeScript + TinyVue,后端基于 NestJS
  3. 强大的功能:支持组件粒度的权限管理、页签模式、多级菜单、多种布局方式、个性化主题、国际化、Mock 数据等丰富的特性,开箱即用
  4. 使用成本低:支持在线方式快速配置角色、用户、菜单、权限、国际化词条,无需写代码,用户使用成本低,没有开发基础的设计师、产品经理也能操作
  5. 开发者友好:支持 ViteWebpackRspackFarm 等多种构建工具

TinyPro 提供了 NestJS 后端,将菜单、路由、国际化词条、角色、用户、权限等内容都放到了后端,用户可以通过在线的方式配置菜单、路由、国际化词条等内容,这样做有以下好处:

  • 前端工程师只需要专注于构建前端页面,配置菜单、国际化词条等工作可以交给管理员
  • 管理员对系统有更多的控制权,功能模块的上线不需要依赖于前端开发

1.png

2 项目生成

TinyPro 这个后台管理模板这么好,我要怎么才能“得到”它呢?

只需要使用 tiny init pro 这一行命令就可以初始化一个!

# 安装 TinyCLI 脚手架
npm install @opentiny/cli -g

# 初始化 TinyPro 项目
tiny init pro

按照以下方式进行选择:

* 请输入项目名称: demo-tiny-pro
* 请输入项目描述: 基于TinyPro套件创建的中后台系统
* 请选择您希望使用的客户端技术栈: vue
* 请选择您希望使用的服务端技术栈: Nest.js
* 请选择你想要的构建工具:  Vite
* 请确保已安装数据库服务(参考文档 https://www.opentiny.design/tiny-cli/docs/toolkits/pro#database): 已完成数据库服务安装,开始配置
* 请输入Redis地址: localhost
* 请输入Redis端口: 6379
* 请选择数据库类型: MySql
* 请输入数据库地址: localhost
* 请输入数据库端口: 3306
* 请输入数据库名称: order_sys
* 请输入登录用户名: root
* 请输入密码: [input is hidden] 

需要注意的是:

  • MySQL 地址是 localhost,端口是 3306(默认),数据库名称是 order_sys
  • Redis 地址是 localhost,端口是 6379(默认)

最后输入的密码是 MySQL 数据库 root 用户的密码。

2.png

3 本地启动

初始化之后的项目主要包含两个目录:nestJs 和 web。

nestJs 是后端代码,web 是前端代码。

3.1 启动后端

我们先来启动后端。

后端使用的是 NestJS,数据库使用的是 MySQL 和 Redis,在启动 NestJS 服务之前,需要先安装和启动 MySQL 和 Redis 数据库。

启动后端之前

  • 确保 MySQL 和 Redis 数据库均已启动。
  • 连接数据库,并使用 create database order_sys; 命令创建一个空数据库。
  • 修改 nestJs/.env 文件中的 DATABASE_SYNCHRONIZE = true
# 进入 nestJs 目录
cd nestJs

# 安装依赖
npm i

# 启动后端
npm start
$ npm start
> nest start

webpack 5.87.0 compiled successfully in 5780 ms
[Nest] 84481  - 2025/04/04 15:10:48     LOG [NestApplication] Nest application successfully started +2ms
Application is running on: http://[::1]:3000

后端启动成功!

3.2 启动前端

启动前端之前,请确保后端已经成功启动。

启动前端和启动后端的步骤基本上是一样的。

# 进入 web 目录
cd web

# 安装依赖
npm i

# 启动前端
npm start
$ npm start
> vite --config ./config/vite.config.dev.ts --port 3031


  VITE v4.5.5  ready in 2051 ms

  ➜  Local:   http://localhost:3031/
  ➜  Network: use --host to expose
  ➜  press h to show help

前端启动成功!

前后端都成功启动之后,就可以访问:http://localhost:3031/

体验 TinyPro 后台管理系统的使用啦!

TinyPro 后台管理系统的效果如下:

3.png

4 如何使用

前面我们已经启动了 TinyPro,接下来就带大家一起体验下如何使用。

使用之前需要先将 nestJs/src/app.module.ts 文件中的以下代码删掉。

@Module({
  ...,
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
-   {
-     provide: APP_GUARD,
-      useClass: RejectRequestGuard,
-   },
    {
      provide: APP_GUARD,
      useClass: PermissionGuard,
    },
  ],
})
export class AppModule implements OnModuleInit {
  // ...
}

接下来将由 TinyPro 核心贡献者 GaoNeng-wWw 带大家使用以下功能。

  1. 如何增加用户

  2. 如何增加菜单

  3. 如何进行权限管理

    1. 创建权限

      1. 组件级权限管理
      2. 页面级权限管理
    2. 创建用户

    3. 分配权限给用户

  4. 如何为角色绑定菜单

  5. 创建好的国际化词条如何在前端中使用

具体演示请看以下视频(从 10:30 开始):TinyPro 使用指南:手把手带你本地启动 TinyPro,在线创建菜单、国际化,实现组件级权限管理

5 二次开发

项目初始化之后,我们可以看下它的目录结构。

4.png

接下来我将以新增订单管理模块前后端为例,给大家演示如何基于 TinyPro 进行二次开发。

5.png

具体演示请看以下视频(从 0:12:24 开始):TinyPro 二次开发教程:手把手带你基于 TinyPro 搭建订单管理模块前后端

本文对应的 视频1 | 视频2 | PPT,欢迎大家观看视频和 PPT。

往期文章

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网opentiny.design

OpenTiny 代码仓库github.com/opentiny

TinyVue 源码github.com/opentiny/ti…

TinyEngine 源码: github.com/opentiny/ti…

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

Vue2源码记录3

作者 醋醋
2025年4月7日 18:46

初始化合并配置

new Vue有两种场景:主动调用new Vue(options)实例化一个Vue对象;实例化子组件。 这两个场景会涉及到配置合并的操作。

// 组件初始化时的配置合并(通过判断 options 上有没有 _isComponent 属性来确定是否是组件)
    if (options && options._isComponent) {
      /**
       * 每个子组件初始化时走这里,这里只做了一些性能优化
       * 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
       */
      initInternalComponent(vm, options)
    } else {
      // new Vue 时的配置合并(new Vue 传入的是用户配置,需要和系统配置合并)
      // 进行 options 的合并,并挂载到 Vue.$options 上,那么 $options.data 可以访问到 data
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

子组件的配置合并

// 组件的配置合并
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // vm.constructor为子组件的构造函数Sub
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

在options中添加了一些parent(父Vue实例)和parentVnode(父VNode实例)及其部分属性。

主动调用配置合并

Vue初始的options

  // 主要将是 components、directives、filters 挂载到 Vue.options
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // 将 Vue 构造函数挂载到 Vue.options._base 上
  Vue.options._base = Vue

  // 给 Vue.options.components 添加内置组件,`<keep-alive>`、`<transition>` 和 `<transition-group>`
  extend(Vue.options.components, builtInComponents)

得到: Vue.options.components = {} Vue.options.directives = {} Vue.options.filters = {}

// 合并两个对象
// 如果子选项与父选项存在相同配置,子选项的配置会覆盖父选项配置
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  // 如果子选项是函数,那么取 child.options
  if (typeof child === 'function') {
    child = child.options
  }

  // 标准化 props、inject、directive 选项,方便后续程序的处理
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  // 对于 child 继承过来的的 extends 和 mixins,分别调用 mergeOptions,合并到 parent 中,递归
  // 被 mergeOptions 处理过的会有 _base 属性
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // 遍历父选项
  for (key in parent) {
    mergeField(key)
  }

  // 遍历子选项,如果父选项不存在该配置,那么合并
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // 合并选项,父子选项有相同选项,子选项覆盖父选项
  function mergeField (key) {
    // 合并策略,data、生命周期、methods 等合并策略不一致
    const strat = strats[key] || defaultStrat
    // 执行合并策略
    // 虽然不同情况合并策略不一样,但是都遵循一条原则:如果子选项存在则优先使用子选项,否则使用父选项
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

不同类型的合并策略

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

// data 合并策略
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

/**
 * Hooks and props are merged as arrays.
 */
// 生命周期合并策略
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */
// component、directive、filter 合并策略
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
// watch 合并策略
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */
// props、methods、inject、computed 合并策略
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

// provide 合并策略
strats.provide = mergeDataOrFn

/**
 * Default strategy.
 */
// 默认合并策略
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

总结: 编程思想:自身定义一些默认配置,在初始化时传入定义的配置,然后按照合并策略合并配置,定制化不同需求。

react入门(上)

作者 dleei
2025年4月7日 18:33

react是什么?

React 是一个开源的 JavaScript 库,由Facebook开发,主要用于构建用户界面,特别是复杂的单页应用程序(SPA)。它专注于视图层,即用户界面的开发,可以与多种库或框架结合使用,以管理应用程序的状态和数据流动。

React 的特点:

  1. 虚拟 DOM

    React 引入了虚拟 DOM 的概念,这是一种内存中的轻量级数据结构,表示实际 DOM 的抽象。当应用状态改变时,React 会高效地计算出虚拟 DOM 的最小变更量,并将这些变更应用到实际 DOM 上,从而大幅提升了性能。

  2. 组件化开发

    React 鼓励采用组件化的方式来构建 UI,即将 UI 分解为可复用的独立组件。每个组件负责管理自己的状态(state)和属性(props),这样可以提高代码的模块化程度,易于维护和重用。

  3. 单向数据流

    React 推崇一种单向数据流的架构,数据通常自上而下地传递给组件树,这简化了状态管理和问题追踪,减少了数据流动的复杂性。

  4. JSX

    JSX 是一种语法扩展,允许在 JavaScript 中混写 HTML 样式的代码。这使得描述 UI 结构变得更加直观和简洁,同时也方便静态代码分析工具进行检查。

  5. 服务器端渲染

    React 支持服务器端渲染(SSR),这对于改善首屏加载速度和搜索引擎优化(SEO)非常有利。

React 的优势:

  • 性能优化:通过虚拟 DOM 减少了直接操作实际 DOM 的次数,提升了应用运行效率。
  • 易学易用:React 的学习曲线相对较平缓,开发者可以快速上手并开始开发。
  • 强大的生态系统:React 拥有庞大的社区支持和丰富的第三方库,如 Redux、React Router 等,可以满足各种开发需求。
  • 代码复用与模块化:组件化的开发方式促进了代码的复用,使得开发大型项目更加高效。
  • 兼容性 和灵活性:React 不限定于特定的技术栈,可以与多种后端技术和前端库集成,提供了高度的灵活性。
  • React Native:React 的理念还延伸到了原生移动应用开发,React Native 允许使用相同的开发模式来构建原生移动应用,实现了跨平台开发能力。

这些特点和优势使得 React 成为了现代Web开发中极为流行的工具之一。

初始化一个react项目

这里以CRA为例,当然你可以选择任何你喜欢的工具来搭建,Vite或是Rsbuild

输入以下命令来创建一个新的React项目,其中react-demo替换为你想要的项目名称:

npx creact-react-app react-demo

使用 npx 命令会自动安装React及相关依赖,并设置好项目的基本结构

cd 切换到创建的文件夹

使用 npm starrt 启动项目

项目的目录结构

react-demo
    ├─ node_modules
    ├─ public
    ├─ favicon.ico
    ├─ index.html
    ├─ logo192.png
    ├─ logo512.png
        ├─ manifest.json
        ├─ robots.txt
    ├─ src
        ├─ App.css
        ├─ App.js
    ├─ App.test.js
    ├─ index.css
    ├─ index.js
    ├─ logo.svg
    ├─ reportWebVitals.js
        ├─ setupTests.js
    ├─ package.json   

下边我们分别来说一下每个文件(夹)的作用:

Node_modules

node的包目录,项目所依赖到的所有第三方包,没啥可说的

Public

public用来存放首页模板及静态资源,该目录中除了index.html都可以删除

  • index.html 首页模板(不能删)
  • favicon.ico 收藏夹图标(可以删,开发中会替换为自己的图标)
  • logoxxx.png React的Logo(删)
  • manifest.json(PWA的配置文件,大部分情况没啥用,删)
  • robots.txt(搜索引擎配置文件,可删)

Src

源码目录,我们写代码就是在这里

index.js

项目入口文件,不能删。

index.css

index.js的样式表,可改可删

App.js

主组件,可改可删

App.css

App.js的样式表,可改可删

xxx.test.js

带有test的都是单元测试的文件,可删

reportWebVitals.js

应用性能统计相关的代码,简单说用户在使用应用时,该模块可以统计用户加载应用所花费的时间,并且根据需要发送给统计服务器,如果没有相关需求可删。

编写我们的第一个react应用,浅尝一下

// app.tsx
function App() {
  return <div className="App">你好,这是我的第一个react项目!</div>;
}

export default App;
import { createRoot } from "react-dom/client"; // react18 的新方法, 创建根节点,替代原来的 createDOM

import App from "./App.tsx";  // 导入 app 组件,也是入口文件和顶级组件
/**
createRoot 接收一个 DOM 元素作为参数,返回一个根节点,在后面继续链式调用 render 方法渲染根组件到获取的 dom 元素中
*/
createRoot(document.querySelector("#root")).render(<App />);

createRoot()

createRoot(domNode, options?)

domNode:一个 ,DOM 元素React 将为这个 DOM 元素创建一个根节点然后允许你在这个根节点上调用函数

options?:可选的根节点配置对象

返回值

createRoot 返回一个带有两个方法的的对象,这两个方法是:renderunmount

unmount()

调用 unmount 以销毁 React 根节点中的一个已经渲染的树

返回值

返回 undefined

render()

用来将React元素渲染到根元素中

首次调用 root.render 时,React 会先清空根节点中所有已经存在的 HTML,然后才会渲染 React 组件

重复调用会在内部进行diff算法,将两次渲染结果进行比较,只修改发生变化的部分,避免没必要的更新,提高性能

react17 写法,不建议使用,后续可能会不再维护
import React from "react"; // react 核心包,在 17 之前不手动导入会报错,在 17 之后可选不用手动导入
import ReactDOM from "react-dom"; 
import App from "./App.jsx";  // 导入App组件,作为应用的入口

// 使用ReactDOM.render方法,将<App />组件渲染到id为"root"的DOM元素中
ReactDOM.render(<App />, document.querySelector("#root")); // 接收两个参数要渲染的结构和目标 DOM

在react的核心包中createRoot方法创建根节点,还有一个方法createElement创建react元素,且创建后无法修改,只能覆盖

import { createElement } from 'react-dom/client'
createElemnet('div',{id:'box',type:'div',className:'btn',onClick:()=>{alert('哎呀!你干嘛.')}},'你好!我是蔡徐坤') 

接收三个参数,第一个是创建元素的类型,第二个是元素对应的一些样式的配置对象,第三个参数是要填充的内容

元素名称必须为html格式的小写

标签中的属性class属性使用className,设置事件属性名采用驼峰命名

元素的内容,子元素,直接在后面使用逗号隔开添加

jsx概念

JSX是JavaScript XML的缩写,它是一种用于在React中编写UI组件的语法扩展。JSX允许开发者在JavaScript代码中编写类似HTML的结构,使得编写和阅读React组件更加直观和简洁。虽然它看起来像是在JavaScript中直接写HTML,但实际上,JSX被编译成普通的JavaScript函数调用,这些函数调用会创建虚拟DOM元素树,最终渲染为真实的DOM元素。

jsx本质

JSX并不是标准的JS语法,它是JS的语法扩展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器中运行

jsx是声明式编程,简单理解就是以结果为导向,就像是你的老板,我不管你的过程我只要结果

// 命令式编程
// 使用createElement创建一个react元素,就相当于我告诉你,我给你10块钱,出门右转,到第二个红路灯路口右转,到旁边的商店买一包盐,再怎么怎么回来
const button = createElement('button',{},'按钮')

// 使用jsx 面向结果声明式编程,以结果为导向
//  可以简单理解为,我告诉你家里炒菜现在没盐了,想办法整一包来,我不管你是使用什么方法 
const button = <button>我是按钮</button>

jsx中使用js表达式

在jsx中可以通过 大括号语法{ }识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等

<script setup>
 const app = <div calssName="app">
          // 使用引号传递字符串
          {'php是世界上最好的语言'}
          // 识别变量
          const name = tom
          { name }
          // 函数调用
          const getUserInfo = () => {
              return { name: Tom , age: 18 }
          }
          { getUserInfo() }
          // 方法调用
          { New Date().getDate() }
          //使用js对象
          <div style={{color:'red'}}>Hello World</div> // 外层{ }识别表达式语法,内层{ }为对象
      </div>
</script>

注意:

  • jsx不是字符串,不要加引号

  • jsx中的html标签小写开头,React组件为大写开头

  • jsx有且只有一个根标签,必须正确结束

  • 布尔类型、Null 、直接写对象的形式以及 Undefined 将会忽略

  • if语句、Switch语句、变量声明属于语句,不是表达式,不能出现在{ }中

jsx只允许只有一个根标签,我们也可以使用 的虚拟根标签来包裹内容,最终是不会渲染出来的,可以理解为vue中的template标签

import { Fragment } from "react/jsx-runtime";

export default function App() {
  return (
    <Fragment>
      <h1>Hello, world!</h1>
    </Fragment>
  )
}

简写形式
export default function App() {
  return (
    <>
      <h1>Hello, world!</h1>
    </>
  )
}

上面提到了一些像布尔类型,null和undefined是不会显示的,那如果我们在调试的时候需要显示那该怎么办呢?

通过值 加上空字符串的形式来解决,对象我们可以使用JSON.stringify()转成字符串的形式来显示

export default function App() {
  return (
    <>
     <div>
      { false + ''}
      <br />
      { true + ''}
      <br />
      { undefined + ''}
      <br />
      {JSON.stringify({name: 'tom', age: 30})}
      <br />
     </div>
    </>
  );
}

列表渲染

const heroList = [
  {
    id: 1,
    name: '锐雯',
    lane: '上单',
  },

  {
    id: 2,
    name: '泰拉米尔',
    lane: '上单',
  },

  {
    id: 3,
    name: '奎因',
    lane: '上单',
  },

  {
    id: 4,
    name: '奥恩',
    lane: '上单',
  },

  {
    id: 5,
    name: '亚索',
    lane: '中单',
  },

  {
    id: 6,
    name: '李青',
    lane: '打野',
  },

  {
    id: 7,
    name: '努努',
    lane: '打野',
  },

  {
    id: 8,
    name: '艾希',
    lane: 'ADC',
  },

  {
    id: 9,
    name: '薇恩',
    lane: 'ADC',
  },

  {
    id: 10,
    name: '卡特琳',
    lane: '辅助',
  },

  {
    id: 11,
    name: '莫德凯撒',
    lane: '辅助',
  },
]

function getHeroList() {
  return (
    <ul>
      {heroList.map(hero => (
        <li key={hero.id}>{hero.name}  分路:{hero.lane}</li>
      ))}
    </ul>
  )
}

export default getHeroList;

条件渲染

export const isLogin = false

export selectedValue = 1

export const getOptions = (val)=>{
if (val === 1) {
  return <div>选项1</div>
}else if (val === 2) {
  return <div>选项2</div>
}else {
  return <div>选项3</div>
 }
}

function App() {
    return (
        // 需要判断两个内容的显示隐藏的时候 可以使用 if else 或是使用简短的三元
    <div>{ isLogin ? '登录成功' : '未登录' }</div> // 未登录
        // 只需判断一个内容显示隐藏,无需使用 else 的时候,可以选择逻辑或 && 来判断,当值为真时,显示,否则反之
    <div>{ isLogin && '登录成功!' }</div>
        // 判断多组内容
        //无法在 jsx 内书写 if else 语句 我们通过调用函数来判断,在函数内进行 if else 判断
     <div>{ getOptions }</div>
    )
}

响应事件

function myButton () {
    const handleClick = () => alert('喜中500万,请到缅北kk园区兑换!')
    return (
    <button onClick={ handleClick }>开奖</button>
    )
}

onClick={handleClick} 的结尾没有小括号!不要调用事件处理函数:你只需把函数传递给事件即可。当用户点击按钮时 React 会调用你传递的事件处理函数。

传递参数

通过一个箭头函数的回调来传值

export default function App() {
  const handleClick = (e: any, value: object) => {
    console.log(e, value);
  };

  return (
    <>
      <button onClick={(e) => handleClick(e, {name: '123', age: 123})}>点击按钮</button>
    </>
  );
}

组件点标记写法

有两种方式,一种是对象的方式,一种是函数的形式

函数的形式

const App = () => {
 return (
  <>
  <div>hello</div>
  <User/>
  </>
 )
}

const User = () => {
  return (
    <div>Welcome</div>
  )
}

export default App;

对象的形式

const App = () => {
 return (
  <>
  <div>hello</div>
  <Abc.User/>
  <Abc.Info/>
  </>
 )
}

 const Abc = {
  User() {
    return <div>Welcome</div>
  },
  Info() {
    return <div>Info</div>
  },
}

这种好处就是我们可以在一个单独的模块下维护多个属于这一个类型的组件,在使用时就知道这个组件是属于哪一个大模块下的,方便维护

同样你也可以选择使用解构,如果你愿意的话

class和style属性绑定

class类名使用className进行属性绑定

不要使用class,为保留关键字

多类名属性绑定,需要使用到 classNames

npm i classNames

基本使用

import React from 'react';
import classNames from 'classnames';

const MyComponent = () => {
  const classes = classNames('btn', 'btn-primary');
  return <button className={classes}>Click me</button>;
};

export default MyComponent;

在这个例子中,classNames 函数将 'btn''btn-primary' 两个类名组合在一起,然后将组合后的类名应用到按钮上

依据条件添加类名

import React, { useState } from 'react';
import classNames from 'classnames';

const MyComponent = () => {
  const [isActive, setIsActive] = useState(false);

  const toggleActive = () => {
    setIsActive(!isActive);
  };

  const classes = classNames('btn', {
    'btn-active': isActive,
  });

  return (
    <button className={classes} onClick={toggleActive}>
      {isActive ? 'Active' : 'Inactive'}
    </button>
  );
};

export default MyComponent;

这里,classNames 函数接收一个对象作为参数,对象的键是类名,值是一个布尔值。如果布尔值为 true,则对应的类名会被添加到最终的类名列表中;如果为 false,则不会添加

组合多个条件和类名

import React, { useState } from 'react';
import classNames from 'classnames';

const MyComponent = () => {
  const [isActive, setIsActive] = useState(false);
  const [isDisabled, setIsDisabled] = useState(false);

  const toggleActive = () => {
    setIsActive(!isActive);
  };

  const toggleDisabled = () => {
    setIsDisabled(!isDisabled);
  };

  const classes = classNames('btn', {
    'btn-active': isActive,
    'btn-disabled': isDisabled,
  });

  return (
    <div>
      <button className={classes} onClick={toggleActive}>
        {isActive ? 'Active' : 'Inactive'}
      </button>
      <button onClick={toggleDisabled}>
        {isDisabled ? 'Enable' : 'Disable'}
      </button>
    </div>
  );
};

export default MyComponent;

在这个例子中,classNames 函数根据 isActiveisDisabled 两个状态变量的值来动态地组合类名

sttyle属性绑定

使用jsx语法,接受对象,连词使用大写方案

 <h1 className='title' style={{color: 'red', fontSize: '30px'}}>Vite + React</h1>

这个一般很少写,都是独立放在一个×××.module.css 文件里面,再进行引入

css_in_js

styled-components

npm i styled-components

基本使用

// app.tsx
import styled from 'styled-components'

const AppWrapper = styled.div`
  .title {
    font-size: 34px;
    color: red;
  }

  .footer {
    font-size: 14px;
    color: #999;
    text-align: center;
    margin-top: 20px;
  }
`

export default AppWrapper
import AppWrapper from './style';

function App() {

  return (
    <>
    <AppWrapper>
      <div className="title">hello React</div>
      <div className="footer">I am a footer</div>
    </AppWrapper>
    </>
  )
}

export default App

它会返回一个组件,可以理解为是一个容器组件,在里面进行内容布局

按照像 scss 和 less 那样实现嵌套

div后面直接写`` 为ES6的标签模板字符串

const name = 'Tom'
const age = 18

function printInfo (...arg: any[]) {
  console.log(arg)
  }
  
  printInfo`hello my name is ${name}, age is ${age}` 

可以理解为通过函数的形式处理模板字符串,实际上就是对函数的调用,接收处理后的模板内容作为参数

第一个参数是静态的模板内容文字分割后的数组

后续参数依次为模板字符串中每个插值表达式(如 ${name}${age})的计算结果

Css Module方案

/* styles.module.css */
.container {
  background-color: lightblue;
  padding: 20px;
}

.title {
  color: darkblue;
  font-size: 24px;
}
import React from 'react';
import styles from './styles.module.css';

const MyComponent = () => {
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Hello, CSS Modules!</h1>
    </div>
  );
};

export default MyComponent;

组件

组件化是一种分而治之的思想:

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展

但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

React组件相较于Vue更加的灵活和多样,可以按照不同的方式划分成很多类组件

函数式组件

定义

  • 使用普通JavaScript函数定义组件。
  • 早期仅用于无状态UI展示(通过props接收数据)。
  • 自React 16.8引入Hooks后,函数式组件可以管理状态和生命周期。

无状态

function Greeting(props) {
   return <h1>Hello, {props.name}!</h1>; // 仅依赖props
}

有状态

import { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // 使用useState管理状态

  useEffect(() => {
    console.log("组件已挂载"); // 使用useEffect模拟生命周期
  }, []);

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

特点

  • 简洁性:代码更短,逻辑更清晰。
  • Hooks 支持:通过useStateuseEffect等管理状态和生命周期。
  • React 推荐:现代React开发中优先使用函数式组件。

class类组件

定义

  • 使用ES6的class语法,继承自React.Component
  • 必须包含render()方法,返回JSX元素。
  • 可以访问生命周期方法(如componentDidMountcomponentDidUpdate)和内部状态(this.state
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 }; // 内部状态
  }

  componentDidMount() {
    console.log("组件已挂载"); // 生命周期方法
  }

  render() {
    return (
      <div>
        <p>计数:{this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          增加
        </button>
      </div>
    );
  }
}

特点

  • 有状态:通过this.state管理状态。
  • 生命周期:可以控制组件的挂载、更新、卸载等阶段。

生命周期

React 的生命周期指的是组件从创建到销毁的整个过程中,在不同阶段自动触发的特定方法。这些方法允许你在组件不同阶段执行逻辑(如数据初始化、状态更新、DOM 操作、资源清理等)

生命周期主要针对类组件,但在函数式组件中可以通过 Hooks(如 useEffect)模拟类似行为。

类组件生命周期

类组件的生命周期分为三个阶段:挂载(Mounting)更新(Updating)卸载(Unmounting)

  1. 挂载阶段(Mounting)

组件首次被创建并插入 DOM 时触发。

  1. constructor()

    1. 作用:初始化状态(this.state)和绑定事件方法(如 this.handleClick = this.handleClick.bind(this))。
    2. 注意:必须调用 super(props),否则 this.props 将未定义。
constructor(props) {
  super(props);
  this.state = { count: 0 };
  this.handleClick = this.handleClick.bind(this);
}

static getDerivedStateFromProps(props, state)(不常用)

  • 作用:在渲染前根据新的 props 更新 state(需要返回新的 state 对象或 null)。
  • 注意:适用于 props 变化时需要同步更新 state 的特殊场景。

render()

  • 作用:返回 JSX 元素,描述组件的 UI。
  • 规则:必须为纯函数(不修改 state,不直接操作 DOM)。

componentDidMount()

  • 作用:组件挂载到 DOM 后触发,适合执行副作用操作(如网络请求、订阅事件、操作 DOM)。
componentDidMount() {
  fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => this.setState({ data }));
}

更新阶段(Updating)

组件因 propsstate 变化而重新渲染时触发。

static getDerivedStateFromProps(props, state)

  • 同挂载阶段,但发生在更新阶段。

shouldComponentUpdate(nextProps, nextState)

  • 作用:决定组件是否需要重新渲染(返回 truefalse)。
  • 用途:性能优化,避免不必要的渲染。
shouldComponentUpdate(nextProps, nextState) {
  return nextState.count !== this.state.count; // 仅当 count 变化时重新渲染
}

render()

  • 重新渲染 UI。

getSnapshotBeforeUpdate(prevProps, prevState)(不常用)

  • 作用:在 DOM 更新前捕获一些信息(如滚动位置)。
  • 返回值:传递给 componentDidUpdate 的第三个参数。
getSnapshotBeforeUpdate() {
  return this.listRef.scrollHeight; // 返回滚动高度
}
  1. 作用:DOM 更新后触发,适合执行依赖新 DOM 的操作(如更新第三方库)。
componentDidUpdate(prevProps) {
  if (this.props.id !== prevProps.id) {
    this.fetchData(this.props.id); // props.id 变化时重新获取数据
  }
}

卸载阶段(Unmounting)

组件从 DOM 中移除时触发。

componentWillUnmount()

  • 作用:清理资源(如取消网络请求、移除事件监听、清除定时器)。
componentWillUnmount() {
clearInterval(this.timerID); // 清除定时器
  window.removeEventListener('resize', this.handleResize);
  }

函数式组件生命周期

函数式组件通过 Hooks 模拟生命周期行为,核心是 useEffect

1. useEffect 的三种模式

  • 挂载阶段:依赖项数组为空 [],仅在组件挂载时执行。
  • 更新阶段:依赖项数组包含变量(如 [count]),变量变化时触发。
  • 卸载阶段:通过返回一个清理函数(cleanup function)模拟 componentWillUnmount
import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 模拟 componentDidMount + componentDidUpdate
  useEffect(() => {
    console.log('组件挂载或 count 更新');
    return () => {
      console.log('清理操作(如卸载时)');
    };
  }, [count]); // 依赖项数组控制触发时机

  return <button onClick={() => setCount(count + 1)}>点击 {count}</button>;
}

常见场景对照

类组件生命周期 函数式组件等效写法
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {}, [dep])
componentWillUnmount useEffect(() => { return cleanup }, [])
shouldComponentUpdate React.memo 或 useMemo

组件通讯

父向子通讯

react中不支持子向父传值

传递给子组件的数据是只读的,不允许进行修改

传递字符串,变量

const App = () => {
  const text = '你好 React!'
  return (
    <>
      <div>
        父组件
        <User
          name='张三'
          text={text}
        /> // 在父组件中使用属性向子组件传值
      </div>
    </>
  )
}

在子组件中可以使用 porps 进行接收或是对其进行解构渲染,结果是以对象的形式进行包裹

const User = ({ name, text }) => {
  return (
    <>
      <div>子组件:user界面</div>
      <div>{name}</div>
      <div>{text}</div>
    </>
  )
}
export default App

传递函数,事件

事件

const App = () => {
  const getMsg = () => {
    alert('调用了父组件的方法')
  }
  return (
    <div>
      <h1>父组件</h1>
      <Child childClick={getMsg} />  // 这里的 childClick 相当于就是一个属性用来传递数据的,所以在点击时并不会触发点击事件
    </div>
  )
}
// 使用解构接收这个事件,并绑定在子组件的 button 按钮上
 const Child = ({ childClick }) => {
  return (
    <div>
      <h1>子组件</h1>
      <button onClick={childClick }>点击</button>
    </div>
  )
}

export default App

自定义函数

const App = () => {
  const getData = data => { // 接收子组件传递来的数据并打印
    console.log(data)
  }
  return (
    <div>
      <h1>父组件</h1>
      <Child childData={getData} />  // 向子组件传递了一个自定义函数
    </div>
  )
}
// 使用解构接收这个函数
const Child = ({ childData }) => {
  const text = '来自遥远的子组件的数据'
  return (
    <div>
      <h1>子组件</h1>
      <button onClick={childData(text)}>点击</button> // 在子组件中点击触发这个自定义函数,并传递数据
    </div>
  )
}

export default App

使用扩展运算符批量传递数据

const App = () => {
  const data = {
    name: '张三',
    age: 18,
    gender: '男',
    address: '北京市',
    hobby: ['篮球', '足球', '游泳'],
  }
  return (
    <div>
      <h1>父组件</h1>
      <Child {...data} />
    </div>
  )
}

const Child = ({ name, age, gender, address, hobby }) => {
  return (
    <div>
      <h1>子组件</h1>
      <h3>{name}</h3>
      <h3>{age}</h3>
      <h3>{gender}</h3>
      <h3>{address}</h3>
      <ul>
        {hobby.map((item, index) => {
          return <li key={index}>{item}</li>
        })}
      </ul>
    </div>
  )
}

export default App

特殊的children属性

当内容嵌套在子组件中的标签中时,父组件会在children属性中进行接收

const Son = ({children}) => {
  return(
    <div>{children}</div>
  )
}

const App = () => {
  return (
    <>
      <Son>
        <div>子组件中的数据</div>
      </Son>
    </>
  )
}

export default App

子向父通讯

父组件

import { useState } from 'react'
import Son from './Son'

const App = () => {
  const [msg, setMsg] = useState('hello')
  const getMsg = msg => {
    setMsg(msg)
  }
  return (
    <>
      <div>
        <span>{msg}</span>
        <Son onGetMsg={getMsg} />
      </div>
    </>
  )
}

export default App

子组件

 // 在子组件中调用父组件的函数并传递实参
 const Son = ({onGetMsg}) => {
  const msgText: String = 'hello world'
  return (
    <>
      <div>
        this is Son:
        <button onClick={()=>onGetMsg(msgText)}>send</button>
      </div>
    </>
  )
}

export default Son

兄弟组件通讯(也就是下面案例中的组件共享数据)

借助状态提升,通过数据下放,实现兄弟组件之间的数据通讯

在a页面点击发送数据到父组件,再由父组件传递数据给b页面实现数据传递

父组件

import { useState } from 'react'
import A from './A'
import B from './B'

const App = () => {
  const [msg, setMsg] = useState("")

  const getMsg = (text)=> {
    setMsg(text)
  }

  return (
    <>
    <A onGetMsg={getMsg} />
    <B msg={msg}/>
    </>
  )
}

export default App

a页面

const A  = ({onGetMsg})=> {
  const aText = 'this is a text'
  return (
    <>
    <div>page a</div>
    <button onClick={()=> onGetMsg(aText)}>send</button>
    </>
  )
}

export default A; // export default A;

b 页面

const B  = ({msg})=> {
  return (
    <>
    <div>page b</div>
    <div>a 页面的数据:{msg}</div>
    </>
  )
}

export default B; // export default B;

传递设置默认值

const App = () => {
  const number = 100
  const data = 'App组件数据内容'
  return (
    <div>
      <h1>父组件</h1>
      <Child
        number={number}
        data={data}
      />
    </div>
  )
}

方式一:使用 es6 添加默认值的方式,直接在后面赋值

const Child = ({ number = 0, data = '默认显示的数据' }) => {
  return (
    <div>
      <h2>子组件</h2>
      {number}
      <hr />
      {data}
    </div>
  )
}

const Child = ({ number, data }) => {
  return (
    <div>
      <h2>子组件</h2>
      {number}
      <hr />
      {data}
    </div>
  )
}

方式二: 使用 react 提供的defaultProps 属性,在组件中添加默认值

Child.defaultProps = {
  number: 0,
  data: '默认显示的数据',
}
export default App

复杂组件之间的数据通讯

import { createContext, useContext } from 'react'
//  1. 使用 createContext 创建一个上下文对象
//  2. 定义一个 Provider 组件,用于提供数据,在顶层组件使用
//  3. 在需要使用的地方通过 useContext 获取上下文中提供的对象,在需要使用数据的地方使用
const Context = createContext('')

const App = () => {
  const msg = '父组件的数据'
  return (
    <div>
      <Context.Provider value={ msg }> // 顶层组件提供数据
        父组件
        <Son />
      </Context.Provider>
    </div>
  )
}

const Son = () => {
  return (
    <div>
      子组件
      <Grandson />
    </div>
  )
}

const Grandson = () => {
  return (
    <div>
      孙子组件
      <Lower />
    </div>
  )
}

const Lower = () => {
  const value = useContext(Context)  // 使用 useContext 获取上下文中的数据
  return (
    <div>
      底层组件
      {value}
    </div>
  )
}
export default App

常用的 react hook

useState

vue3 中我们使用 refreactive 来声明响应式数据

而在 react 中我们使用 useState 钩子,来创建一个响应式数据,通过 count 来访,通过 setCount 来修改数据

import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
// 你将从 useState 中获得两样东西:当前的 state(count),以及用于更新它的函数(setCount)。你可以给它们起任何名字,但按照惯例会像 [something, setSomething] 这样为它们命名
  return (
      <div>
        <div>{count}</div>
        <button onClick={() => setCount(count + 1)}>按钮</button>
      </div>
  )
}

useMemo

import { useMemo, useState } from 'react'
import './App.css'

const App = () => {
  const [count, setCount] = useState(1)
  // useMemo 有两个参数,
  // 第一个参数是一个计算函数,返回一个经过计算缓存之后的值
  // 第二个参数是一个依赖数组,当依赖数组中的值发生变化时,才会重新计算缓存值

  const double = useMemo(() => {
    return count * 2
  }, [count])
  return (
    <>
      <button onClick={() => setCount(count + 1)}>double</button>
      <div className='fs20'>{double}</div>
    </>
  )
}

export default App

useEffect

useEffect 在函数组件中执行副作用操作,如数据获取、订阅或手动更改 DOM。它可以看作是 componentDidMount, componentDidUpdate, 和 componentWillUnmount 的组合

简单理解就是 vue 中的 onMounted 周期

useEffect(()=>{
// 需要执行的操作
},[])
  • 参数一是一个函数,可以把它叫做副作用函数,在里面放入需要执行的操作
  • 参数二是一个依赖项数组,可选,不同的依赖项会影响第一个参数函数的执行
依赖项 函数执行时机
不传 组件初始化渲染,组件更新时会再次执行
空数组 只会在初始化渲染执行一次
添加特定依赖项 组件初始化渲染,特定依赖项更新时执行

useEffect清除副作用

在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用

可以理解为 vue 生命周期中的 beforedestory

只需要在实现副作用逻辑的操作里面后面 return 一个函数里面写上清除副作用的逻辑就行

useEffect(()=>{
// 实现副作用的逻辑操作
return ()=> {
// 清除副作用逻辑
}
},[])
import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 启动一个定时器
    const timerID = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    // 返回一个清理函数,用于清除定时器
    return () => {
      clearInterval(timerID);
    };
  }, []); // 空数组作为依赖项,意味着该 effect 只在组件挂载和卸载时执行

  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}

export default MyComponent;

useReducer

和useState类似用于管理相对复杂的状态数据

  1. 定义 reducer 函数,依据 action 对象的不同状态返回对应的 state
  2. 调用 useReducer hook函数,传入定义好的 reducer 函数和默认初始状态
  3. 使用时调用dispatch函数传入一个 action 对象,对象中包含一个 type 属性,reducer 函数会依据这个 type 属性进行不同的操作
  import { useReducer } from 'react'

  function reducer(state,{type}) {
    if(type === 'inc') {
      return state + 1
    } else if(type === 'dec') {
      return state - 1
    } else {
      return state
    }
   }

function App() {
  const [state,dispatch] = useReducer(reducer,0)

  return (
    <>
      <button onClick={() => dispatch({type: 'dec'})}>-</button>
      &nbsp;
      <span>{state}</span>
      &nbsp;  
      <button onClick={() => dispatch({type: 'inc'})}>+</button>
    </>
  )
}
export default App

useCallback

useCallback 返回一个 memoized 回调函数,避免在每次渲染时重新创建函数,适用于传递给子组件的回调函数,以优化性能

  • 在需要缓存函数的地方调用useCallBack ,传入需要缓存的函数及依赖项数组
  • 当数组中的值变化时,重新才会创建回调函数
import React, { useState, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return <Child onClick={increment} />;
}

function Child({ onClick }) {
  return <button onClick={onClick}>增加</button>;
}

export default Parent;

useRef

用于绑定dom对象,访问组件的实例,类似于vue中的ref

使用时使用 .current 属性进行访问

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 直接访问 DOM 元素
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

forwardRef

使用 ref 暴露 dom 节点给父组件

在vue3中是需要使用defineexpose来暴露节点,然后通过组件的ref来获取想要的信息

父组件

function App() {
 const inputRef = useRef(null)
 return (
  <>
  <Son ref={inputRef} />
  <button onClick={()=>inputRef.current.focus()}>获取焦点</button> //通过.current获取到组件实例来进行相应的操作
  </>
 )
}

export default App

子组件

 // forwardRef 返回的还是一个组件
const Son = forwardRef((props,ref)=> {
  return <input ref={ref} />
})

uselmperativeHandle

通过ref暴露子组件的方法

父组件


function App() {
 const inputRef = useRef(null)
 // 调用子组件的方法获取焦点
 const focusHandler = () => inputRef.current.focusIpt()
 return (
  <>
  <Son ref={inputRef} />
  <button onClick={focusHandler}>获取焦点</button>
  </>
 )
}

export default App

子组件

 // forwardRef 返回的还是一个组件
const Son = forwardRef((props,ref)=> {
// 获取dom实现聚焦
const iptRef = useRef(null)
const focusIpt = () => iptRef.current.focus()
// 把聚焦方法暴露出去
useImperativeHandle(ref,()=> {
  return {
    focusIpt
  }
})
  return <input ref={iptRef} />
})

数据不可变性

在 React 中,保持 State 的不可变性(Immutability) 是指修改状态时,不直接改变原始状态对象或数组,而是创建一个新的副本,并在副本上进行修改。

一、什么是不可变性?

  • 不可变对象:一旦创建,就不能被直接修改(如修改属性、增删元素等)。
  • 修改方式:通过创建对象的新副本,并在副本上修改数据,而不是直接修改原对象。
// ❌ 错误:直接修改原状态
state.user.name = 'Alice';

// ✅ 正确:创建新对象并替换原状态
this.setState({ user: { ...state.user, name: 'Alice' } });

二、为什么必须保持不可变性?

1. React 的更新机制依赖不可变性

  • React 使用浅比较(Shallow Comparison) 判断状态是否变化:

    • 如果直接修改原对象,对象的引用地址不变,React 会认为状态未变化,导致组件不更新。
    • 只有创建新对象,才会触发重新渲染。
// ❌ 错误:直接修改数组
const newTodos = todos;
newTodos.push({ id: 4, text: 'Learn React' });
setTodos(newTodos); // React 检测不到变化,不会更新!

// ✅ 正确:创建新数组
setTodos([...todos, { id: 4, text: 'Learn React' }]);

2. 避免副作用(Side Effects)

  • 直接修改原状态可能导致代码中其他依赖该状态的地方出现意外行为。
  • 不可变性确保状态的每次变化都是可预测的。

3. 支持时间旅行(Time Travel)

  • 在 Redux 或 React 状态管理工具中,不可变性是实现撤销/重做(Undo/Redo)等功能的基石。
  • 每次状态变更都保留完整的历史副本,便于回溯。

4. 性能优化

  • 不可变数据可以快速比较引用,优化 shouldComponentUpdateReact.memouseMemo 的性能判断。
  • 对大型数据结构的深比较(Deep Comparison)成本高,不可变性通过浅比较提升性能。

三、如何保持不可变性?

1. 修改对象

  • 使用 Object.assign 或扩展运算符(...)创建新对象:
// ✅ 正确:合并对象
const newState = { ...state, age: 25 };

// ✅ 正确:修改嵌套属性
const newUser = { 
  ...state.user, 
  profile: { ...state.user.profile, name: 'Bob' } 
};

2. 修改数组

  • 使用 mapfilterconcatslice 或扩展运算符返回新数组:
// ✅ 添加元素
const newArray = [...array, newItem];

// ✅ 删除元素(过滤)
const newArray = array.filter(item => item.id !== idToRemove);

// ✅ 修改元素
const newArray = array.map(item => 
  item.id === id ? { ...item, value: newValue } : item
);

受控组件/非受控组件

受控组件

受控组件是表单元素的值由 React 组件的状态控制的组件。也就是说,表单元素(如<input><textarea><select>等)的值是从 React 组件的状态中获取,并且其值的变化通过事件处理函数更新状态。受控组件使得数据的流向是单向的,即数据从状态流向表单元素,而表单元素的变化又会更新状态。

import React, { useState } from 'react';

function ControlledInput() {
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('提交的值:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={handleChange}
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default ControlledInput;

在上述代码中,input 元素的值由 value 状态控制,每次输入框内容发生变化时,handleChange 函数会更新 value 状态。这样,输入框的值始终与 value 状态保持一致。

非受控组件

非受控组件是表单元素的值由 DOM 自身控制的组件。在非受控组件中,你可以使用 ref 来获取表单元素的值,而不是通过 React 组件的状态来控制。非受控组件通常在表单元素的值只在提交表单时才需要使用的情况下使用。

import React, { useRef } from 'react';

function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('提交的值:', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default UncontrolledInput;

在上述代码中,使用 useRef 创建了一个 ref 对象 inputRef,并将其传递给 input 元素的 ref 属性。在提交表单时,通过 inputRef.current.value 获取输入框的值

受控组件和非受控组件的对比

  • 数据流向:受控组件的数据流向是单向的,由状态控制表单元素的值;非受控组件的数据由 DOM 自身控制。
  • 状态管理:受控组件需要在 React 组件中管理状态,每次表单元素的值变化时都要更新状态;非受控组件不需要在 React 组件中管理状态,通过 ref 直接获取 DOM 元素的值。
  • 使用场景:受控组件适用于需要实时获取表单元素的值、对表单元素的值进行验证和处理的场景;非受控组件适用于表单元素的值只在提交表单时才需要使用的场景。

高阶组件

在 React 里,高阶组件(Higher-Order Component,简称 HOC)是一种函数,它接收一个组件作为参数,并且返回一个新的组件。高阶组件并非 React API 的一部分,而是从 React 的组合特性里衍生出的一种设计模式。其核心作用在于代码复用、状态抽象与管理、渲染劫持等。

以下是一个简单的登录鉴权高阶组件示例,此高阶组件会对用户的登录状态进行检查,若未登录就会重定向到登录页面,反之则渲染原组件

import React from 'react';
import { useNavigate } from 'react-router-dom';

// 模拟检查用户是否登录的函数
const isAuthenticated = () => {
    // 这里可以根据实际情况修改,比如从 localStorage 中获取 token 等
    return localStorage.getItem('isLoggedIn') === 'true';
};

const withAuth = (WrappedComponent) => {
    return function AuthComponent(props) {
        const navigate = useNavigate();

        if (!isAuthenticated()) {
            // 如果未登录,重定向到登录页面
            navigate('/login');
            return null;
        }

        // 如果已登录,渲染原组件
        return <WrappedComponent {...props} />;
    };
};

export default withAuth;
    

使用示例

假设你有一个受保护的组件 Dashboard,你可以使用这个高阶组件来保护它:

import React from 'react';
import withAuth from './login_auth_hoc';

const Dashboard = () => {
    return (
        <div>
            <h1>欢迎来到首页</h1>
        </div>
    );
};

export default withAuth(Dashboard);

组件间共享数据

import { useState } from 'react';

export default function MyApp() { 
  // 在 MyApp 组件中定义 count 状态和 handleClick 事件处理函数
  const [count, setCount] = useState(0);

  function handleClick() {
    // 当按钮被点击时,更新 count 状态
    setCount(count + 1);
  }

  return (
    <div>
      <h1>数据更新</h1>
      {/* 将 handleClick 函数和 count 状态通过 props 传递给 MyButton 组件 */}
      <MyButton onClick={ handleClick } count={ count }/>
      <br/>
      <MyButton onClick={ handleClick } count={ count }/>
    </div>
  );
}

// MyButton 组件接收 onClick 和 count 作为 props
function MyButton({ onClick, count }) {
  // 当按钮被点击时,调用父组件传递过来的 onClick 函数
  return (
    <button onClick={ onClick }>
      {/* 显示从父组件传递来的 count 值 */}
      当前数字为:{ count }
    </button>
  );
}
  1. 我们将 count 状态和 handleClick 事件处理函数从 MyButton 组件移动到了 MyApp 组件中。这种做法称为“状态提升”,可以将共享的状态和行为放在更高层级的组件中。
  2. MyApp 组件中,我们通过 props 将 handleClick 函数和 count 状态传递给每个 MyButton 组件。这样,每个按钮都可以访问相同的状态和行为。
  3. MyButton 组件中,我们接收 onClickcount 作为 props,并将 onClick 函数绑定到按钮的点击事件上。每个按钮显示的数字都是从父组件传递过来的 count 值。

组件的排列组合

一个父组件里面套了一个子组件,那如果我想在子组件里面接着嵌套其他的功能模块的时候是把它放到子组件的内容里面吗?

我们来 try 一 try

const App = () => {
  return (
    <div>
      <h1>父组件</h1>
      <Child />
    </div>
  )
}

const Child = () => {
  return (
    <div>
      <h2>子组件</h2>
      <Other />
      <List />
    </div>
  )
}

const Other = () => {
  return (
    <div>
      <h3>其他组件</h3>
    </div>
  )
}

const List = () => {
  return (
    <div>
      <h3>列表组件</h3>
    </div>
  )
}

export default App

可以实现效果

这样就会出现一个问题,就是我子组件里面的组件想要去拿取数据只能是在相对于它自己的父级,涉及到了一个作用域的问题

const App = () => {
  const appData = 'App组件数据内容'
  return (
    <div>
      <h1>父组件</h1>
      <Child />
    </div>
  )
}

const Child = () => {
  const childData = 'Child组件数据内容'
  return (
    <div>
      <h2>子组件</h2>
      <Other data={childData} />
    </div>
  )
}

const Other = ({ data }) => {
  return (
    <div>
      <h3>其他组件 :{data}</h3>
    </div>
  )
}

export default App

所以在实际开发中一般使用 props 里面的另外一个属性 children 属性来渲染组件内的任何子元素

注意是小写

const App = () => {
  const data = '父组件数据'
  return (
    <div>
      <h1>父组件</h1>
      <Child>
        <Other data={data} />
      </Child>
    </div>
  )
}

const Child = ({ children }) => {
  return (
    <div>
      <h2>子组件</h2>
      {children}
    </div>
  )
}

const Other = ({ data }) => {
  return (
    <div>
      <h3>其他组件{data}</h3>
    </div>
  )
}

export default App

这样就实现了跨层级之间的数据传递

但是如果我想要传递多个内容并把它放在指定不同的位置,该如何实现呢?

这个和 vue 中的插槽的功能有一些相像,都是向组件内分发内容,不过 vue 是可以通过具名插槽来进行指定的分发和接收

vue 的实现方式

// app.vue
<script setup lang="ts">
  import son from './components/son.vue'
</script>

<template>
  <son>
    <template #header>
      <h1>我是标题</h1>
    </template>

    <template #default>
      <p>我是内容</p>
    </template>

    <template #footer>
      <p>我是底部</p>
    </template>
  </son>
</template>

// son.vue
<template>
  <div class="root">
    <slot name="header"></slot>
    <slot name="default"></slot>
    <slot name="footer"></slot>
  </div>
</template>

react的实现方式

在传递数据的时候是可以传递 jsx 的,我们在传递的时候就直接传递不同的内容,对应进行接收使用

const App = () => {
  return (
    <div>
      <h1>父组件</h1>
      <Child header={<h4>头部标题</h4>} body={<p>内容</p>} />
    </div>
  )
}

const Child = ({ header,body }) => {
  return (
    <div>
      <h2>子组件</h2>
      {header}
      <hr />
      {body}
    </div>
  )
}

export default App

一个组件中与多个相同的组件,数据之间是相互隔离的

import { useState } from 'react';

export default function MyApp() {
  return (
    <div>
      <h1>数据更新</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      当前数据: {count}
    </button>
  );
}

本文为个人学习记录,内容会根据学习进度不定期补充和更新,难免存在一定的疏漏或错误。若发现任何问题,恳请指正与建议

❌
❌