【工作经历/项目经历】-2025

原型大核心

介绍一下墨刀?你对项目的理解

使用react+ts+style-components开发,目前已经累积400w+用户,日活10w左右的原型设计工具,主要用户是产品经理和设计师。

一、主要功能
从左侧面板中拖拽出按钮,图片, 批注等组件,放在页面的编辑区,可以组合成一张高保真的UI原型图,然后使用分享、演示等功能交付到研发手中去开发。它支持点击、双击、右键、拖拽、鼠标悬停
二、数据存储与处理
在数据层**(FlatJson)**,墨刀有多个存储相关的模块,如 sdkStore、cmtStore 和 flatStore 等,这些存储模块用于管理不同类型的数据,包括软件开发工具包数据、注释或评论数据以及扁平化数据等。
数据处理操作包括更新(Update)、更新记录(Update Record)、监听变化(Listen Changes)、缓存(Cache)、格式化 / 解码(Format/Decode)、压缩 / 解压(Compress/Decompress)等,这些操作确保了数据在存储和使用过程中的完整性和高效性。
基于 hotTree 中的数据来进行 ui 层面的渲染,通过渲染组件和相关技术实现界面的呈现。文本编辑器在 ScreenContainer 中,文本编辑功能也是通过特定的组件和数据管理来实现的,确保了用户在界面设计中对文本编辑的需求能够得到满足。

组件怎么更新?

例如,在一个简单的手机应用原型设计中,用户在一个页面上修改了一个按钮的文本内容,从 “提交” 改为 “保存”。首先,该操作会被前端捕获,数据层的相应存储模块会更新该按钮的文本数据,并记录下更新操作。然后,数据会被发送到服务端进行存储。服务端存储成功后,前端通过监听机制得知数据已更新,获取到最新的数据后,在 hotTree 中找到对应的按钮组件节点,更新其文本属性,最后重新绘制该按钮,使其在界面上显示为 “保存”。

技术选项对比?为什么选择Draft.js?

Draft.js的优势

  1. 扩展性强:
    不可变的数据结构 draft.js使用immutable.js提供的数据结构。draft.js中所有的数据都是不可变的。但可以通过 生成一个新的数据副本 来体现“修改”的效果。每次修改都会新建数据,并且内存中会保存原来的状态,方便回到上一步,这里很符合react的单向数据流的设计思路。
    我写代码的时候经常就是 newxx:
	const newContentState === Modifier.insertText(contentState, selectionState, '新文本');  // 插入文本
	const newEditorState = RichUtils.toggleInlineStyle(editorState, 'BOLD');	// 将选中的文字加粗
	const newEditorState = RichUtils.toggleBlockType(editorState, 'header-one');添加区块类型 H1

完全控制的渲染过程 : Draft.js 的内容渲染基于 React,允许用户通过自定义组件来精确控制每一个区块、内联样式、甚至光标行为。

  1. 原生 React 集成:
    Draft.js 是为 React 生态量身定制的,与 React 的组件模型和状态管理高度契合。

在这里插入图片描述
总结:Quill使用起来比 Draft.js 更简单,适合快速开发或对文本编辑需求较简单的项目。

为什么选择?

  1. 业务需求: 项目对复杂需求的支持,Draft.js 能轻松实现自定义区块和样式,支持 React 的组件化开发
  2. 对数据结构的完全控制 : Draft.js 提供对编辑器内部状态(EditorState、ContentState)的完全控制,便于实现高度定制的功能。
  3. 与 React 状态管理工具的协作
    Draft.js 的状态对象(EditorState 和 ContentState)可以很方便地与 Redux、Context API 等状态管理工具结合。例如:
    可以通过 Redux 管理多个编辑器的状态。可以通过 Context API 共享全局状态(如工具栏设置、内容格式等)。
  4. 项目技术栈是 React : Draft.js 的状态管理和渲染逻辑与 React 高度一致,能无缝集成到现有项目中。
  5. 安全性: 可以结构化存储富文本内容,而不需要保存html片段。HTML 容易引入恶意代码(如

draft.js

Draft.js 使用 结构化数据模型(ContentState)来表示富文本内容:

  • 内容被分为块(Blocks)和内联样式(Inline Styles):
  • 每个块(Block)对应一个区块内容(如段落、标题、列表等)。
  • 内联样式(如加粗、斜体)单独存储,不嵌入 HTML 标签
  • 内容结构化存储后,可以方便地序列化为 JSON,而不是直接存储 HTML。
{
  "blocks": [
    {
      "key": "6mgfh",
      "text": "Hello Draft.js",
      "type": "unstyled",
      "depth": 0,
      "inlineStyleRanges": [
        { "offset": 6, "length": 5, "style": "BOLD" }
      ],
      "entityRanges": [],
      "data": {}
    }
  ],
  "entityMap": {}
}

EditorState 是一个不可变对象,专注于数据存储和操作逻辑。
Editor 组件负责内容的展示和交互,基于 React 的渲染机制。

**EditorState **
EditorState 包括的内容大致如下:
(1) 当前文本内容状态(ContentState)
(2) 当前选中内容状态(SelectionState)
(3) 所有的内容修饰器(Decorator)
(4) 撤销和重做栈
(5) 最后一次变更操作的类型。

ContentState
是描述富文本编辑内容状态的对象,你可以认为你打字输入文本内容都是由这个对象下管理控制的。它会分有多个ContentBlock(为简化描述,后文都称作block),多个block实际上又是CharacterMetaData组成的,每一个CharacterMetaData会包含实际的字符以及他的Entity和inline style等信息

SelectionState
一个富文本编辑器,它除了内容状态的描述,它应该还会有光标状态的描述。这个状态可以判断光标是否重合、光标选中的锚点或焦点、选中的起始或终点的偏移量等

Entity
实体对象,它是一个数据结构,包含了一些元数据用来描述一段文本

block是怎么划分的呢?
block可以想象是html的块级元素,列表、段落、代码块都看作是一个block

优化富文本组件?

优化批注富文本的性能是一项综合性工作,需要从多个方面入手,以下是一些具体方法:
渲染优化
虚拟列表渲染:采用虚拟列表技术只渲染可视区域内的批注,当用户滚动时动态加载和卸载批注。如使用react-window或react-virtualized等第三方库,通过合理设置行高和缓冲区大小,可大幅减少 DOM 节点数量,提高渲染性能。
懒加载策略:对批注内容中的图片、视频等资源采用懒加载,在批注进入可视区域时再进行加载,避免一次性加载大量资源导致页面卡顿。同时,对于长文本批注,也可以采用分段懒加载的方式,先显示部分内容,用户点击展开时再加载剩余部分。

数据处理优化
数据缓存:对于经常使用的批注数据,如常用的批注样式、表情符号等,进行本地缓存,避免频繁从服务器获取或重新计算。同时,对已渲染的批注 DOM 节点进行缓存,在批注状态未发生改变时直接复用,减少重新渲染的开销。
批量更新:在处理多个批注的更新操作时,如同时修改多个批注的样式或内容,将这些操作合并为一次批量更新,减少渲染次数。在 Draft.js 中,可以使用ContentState.createFromBlockArray等方法批量更新内容状态,然后一次性触发渲染。
数据精简:在保证批注功能和信息完整的前提下,对批注数据进行精简,去除不必要的冗余信息。例如,对于批注中的文本内容,可以进行压缩或去除多余的空格、换行等,减小数据传输和处理的负担。
事件处理优化
事件委托:将批注的点击、鼠标移动等事件委托给父元素或根元素统一处理,避免为每个批注单独绑定事件,减少事件处理函数的数量,提高事件处理效率。
节流与防抖:对于频繁触发的事件,如窗口滚动、文本输入等,使用节流或防抖技术限制事件的触发频率。在批注富文本中,例如用户快速滚动页面时,对批注的加载和显示事件进行节流或防抖,避免过多的计算和渲染。
代码优化
避免不必要的重渲染:使用shouldComponentUpdate或React.memo等方法对组件进行优化,避免在组件的 props 或 state 未发生变化时进行不必要的重渲染。在批注富文本的组件设计中,精确判断哪些数据变化会影响组件的渲染,只在必要时更新组件。
优化 CSS 选择器和样式计算:减少使用复杂的 CSS 选择器,避免多层嵌套的样式规则,提高浏览器的样式计算效率。同时,尽量使用类名选择器而不是标签选择器,以提高样式的特异性和渲染速度。

优化富文本组件涉及多个方面,包括但不限于:
减少不必要的重新渲染:通过使用 React.memo、shouldComponentUpdate 和虚拟化等技术减少重渲染。
优化 DOM 操作:避免频繁更新 DOM,批量更新样式和使用事件委托。
输入优化:通过防抖(debounce)来减少不必要的计算和更新。
异步计算:使用 Web Workers 来处理复杂计算,避免阻塞主线程。
在这里插入图片描述


如何使用draft-js实现空格标题h1、ul、ol 类似于富文本?

整体实现思路

1. 定义样式
利用 Draft-js 的customStyleMap属性来定义 h1 标题、无序列表(ul)和有序列表(ol)以及它们的子项的样式。使用text-indent实现空格缩进。
2. 切换块类型
通过RichUtils.toggleBlockType方法来切换文本块的类型。当用户选择一段文本并点击相应的按钮时,如 “设置为 h1 标题”“转换为无序列表”“转换为有序列表” 等,调用该方法将文本块的类型分别设置为自定义的 h1 样式、unordered-list-item或ordered-list-item。
3. 处理文本输入与显示:
对于 h1 标题中的空格输入,它有个placeholder:
实现:首先通过 isBlockHeadingType 判断当前文本块是否属于标题类型,blockText === ‘’,会设置一个特定的类名 H1,在其 :before 伪元素的 content 属性中,设置为 attr(data-placeholder)。而如果文本块正在编辑(isComposing 为 true),则只返回普通的 div 包裹 _renderChildren 方法渲染的内容,暂不展示 placeholder,保持正常编辑状态下的显示逻辑。
4. 对键盘事件的监听:
keyBindingFn keyCode === 32(空格) 光标是否在当前 block 中。

难点及解决方案:

  • 样式兼容性和一致性:对于设置字体大小和缩进等关键样式属性,使用 em 或 rem 单位代替 px。

文本块操作的复杂性:

  • 获取文本状态信息:
    在进行文本块切换操作(比如将普通段落转换为 h1 标题或者列表项等)时。通过 getCurrentContent 获取整个编辑器内容的状态,通过 getSelection 获取当前光标所在的选区情况,然后基于这些信息使用 RichUtils.toggleBlockType 来进行文本块类型的切换操作。
  • 处理文本块嵌套情况:
    比如列表项中又嵌套了其他类型的文本块,在处理编辑操作时,需要先判断嵌套结构。通过获取当前文本块及其父级文本块的类型,判断是否处于列表项嵌套场景,进而针对不同情况(嵌套或非嵌套)采用不同的内容状态修改逻辑,确保文本块操作的正确性。
    列表项格式的维护
  • 监听 onReturn 事件并处理列表项格式添加:
    当按下回车键且当前文本块是列表项类型时,通过 addNewListItemFormat 函数,利用 Modifier.insertBlock 等操作在内容状态中插入新的符合要求的列表项格式,保证在按下回车键后能正确添加下一个列表项。
  • 处理列表项删除时格式调整
    当删除列表项时,同样要监听相应的删除操作事件(比如删除键按下等情况),通过判断前后文本块的类型来自动调整格式,确保列表的完整性和连续性。

总的来看: 难点主要体现在样式兼容性和一致性、文本块操作的复杂性以及列表项格式的维护三个方面。不同浏览器对 CSS 样式的渲染差异导致自定义样式显示效果不理想,文本块切换和输入时需考虑多种复杂情况,用户输入和编辑过程中确保列表项格式正确也颇具挑战。 但是有很多case没有考虑到,在前期技术文档编写的时候需要考虑的更多,同时也认识到在前期技术文档编写时应更加全面深入,需充分考虑各种可能出现的 case,可通过参考更多类似项目的技术文档、与团队成员进行头脑风暴等方式,尽可能涵盖各种实际应用场景,从而提高文档的实用性和指导性。

优化:
更全面的测试矩阵: 除了常见的桌面浏览器,还应纳入移动浏览器以及不同操作系统下的浏览器版本进行测试。例如,在移动端要考虑 iOS 系统下的 Safari 以及各安卓系统定制浏览器并针对不同屏幕尺寸和分辨率进行样式检查,可借助工具如 BrowserStack 或者 LambdaTest 等云测试平台,能更便捷地覆盖大量不同的浏览器环境组合,获取更全面准确的样式兼容性情况反馈。
自动化视觉对比: 可以利用如 Applitools、 Percy 等视觉测试工具,在不同浏览器测试时,不仅对比样式属性值,还从视觉呈现角度对比页面截图,自动检测出细微的样式差异,比如元素间距、颜色偏差等肉眼较难精准察觉的问题,这样能更高效地发现潜在的样式不一致情况,而不是仅依赖手动查看和简单的属性值记录。
测试用例评审时 :应该与测试沟通,并自测。
全面的技术选型评估: 参考技术论坛、开源社区等资源,了解其他项目在使用该技术时遇到的问题及解决方法,将相关 case 纳入技术文档。
细化需求规格说明书: 对项目需求进行更细致的拆解和分析,明确每个功能模块的具体要求、输入输出、边界条件等。通过详细的需求规格说明书,为技术文档编写提供更全面的依据,减少 case 遗漏的可能性。

如何在前期技术文档编写中更全面深入地考虑各种可能出现的 case?

  1. 深入了解技术原理和流程
  2. 广泛收集需求和反馈
  3. 进行严谨的场景分析和模拟,包括正常操作流程、异常操作情况、边界条件等。
  4. 持续更新和完善文档,可以建立一个建立动态文档机制。文档更新的流程和规范,明确在什么情况下需要对文档进行修改,由谁来负责更新,以及如何确保更新后的文档能够及时传达给相关人员。

怎么确定是富文本组件引起的卡顿?

  • 首先,我会借助浏览器自带的开发者工具,比如 Chrome 的 Performance 面板。在使用富文本组件进行各类操作,如大量输入文字、频繁切换格式、插入复杂元素等过程中,记录下 FPS(帧率)、CPU 使用率和内存占用等关键性能指标。
    其次,利用 DOM 操作监控手段。通过MutationObserver API 来跟踪富文本组件相关 DOM 节点的变化情况,统计其增删改的频率和数量。若观察到短时间内有大量频繁的 DOM 操作,例如频繁地创建和销毁文本节点、元素节点的样式属性被反复修改等,这极有可能是导致 DOM 卡顿的原因之一。
  • 再者,从事件监听方面着手。检查富文本组件及其相关元素上绑定的事件数量,若存在过多不必要的事件绑定,尤其是针对频繁触发的事件,如鼠标的移动、点击,键盘的输入等,且每个事件都关联着复杂的回调函数,那么当这些事件被大量触发时,就可能导致 DOM 卡顿。通过计算回调函数的执行时间来评估其对性能的影响。
  • 另外,进行渲染性能测试也非常重要。创建一个只包含富文本组件和基本元素的简化测试页面,模拟实际的使用场景进行操作,对比简化页面和完整页面在富文本组件操作时的渲染性能。

计算回调函数的执行时间来评估其对性能的影响 ?

  • 使用console.time()和console.timeEnd()
  • performance.now()是浏览器Performance API提供的方法,它返回一个高精度的时间戳,能够更精确地测量时间
  • 火焰图:火焰图从左到右表示时间的先后顺序,每个长条代表一个函数的执行,长条越长,说明该函数执行所花费的时间越长。一般可以通过函数名称、所在文件路径等信息来判断,如果是自定义的回调函数,你应该知晓其命名

为什么DOM 多就会导致卡顿呢?

  1. 浏览器渲染原理:
    构建 DOM 树
    浏览器在加载 HTML 时,会将 HTML 标签解析成 DOM 节点,构建 DOM 树。当 DOM 元素非常多时,构建 DOM 树的时间就会变长。
    布局和重排
    浏览器需要确定每个 DOM 元素在页面上的位置和大小,这一过程称为布局(layout)。当 DOM 元素数量庞大时,布局计算变得非常复杂。
    绘制和重绘
    浏览器在确定了 DOM 元素的布局后,会将元素绘制(paint)到屏幕上。当 DOM 元素极多时,绘制过程会变得缓慢。
    2.内存占用和性能
    一个简单的
    元素可能在内存中占用几百字节,五六万这样的元素累积起来会消耗相当可观的内存。这可能导致浏览器频繁进行垃圾回收(Garbage Collection,GC)操作,而 GC 操作本身也会影响性能,导致卡顿。
    有五六万的 DOM 元素都绑定了点击事件,当用户在页面上进行操作时,浏览器需要遍历大量的元素来确定事件的触发源,这会导致响应延迟和卡顿。

DOM 的整个生成过程?

  1. 字节流的转换: 当浏览器请求一个网页时,服务器会返回 HTML 文档的字节流,转为字节符号;
  2. 词法分析: 浏览器会将字符流解析成一个个标记(Tokens)。例如,、、
    等标签,以及标签内的文本内容都会被解析成不同的标记。
  3. 语法分析: 浏览器会构建出节点(Nodes),并根据 HTML 的语法规则将节点组织起来。例如,它会识别出是根节点,是的子节点
  4. 构建 DOM 树: 节点按照其在 HTML 文档中的层次关系连接在一起。
  5. 关联外部资源: 在 DOM 树构建过程中或构建完成后,浏览器会识别 HTML 中的外部资源链接,如

富文本组件性能优化?

背景&&现状

埋点方法
手动埋点:在特定代码中直接插入埋点逻辑。
无侵入埋点(自动埋点):通过工具或中间层拦截事件,统一处理。
做了神策的手动埋点:

统计了什么:记录交互事件发生的时间,如编辑开始、拖拽触发、组件加载时间;性能指标;用户行为:交互事件类型,如“组件编辑”、“拖拽开始”;系统环境:浏览器、设备类型、网络状态;
怎么埋点统计: : 一般来说,业务代码写在埋点代码前。
统计编辑事件: const startTime = performance.now(); // 开始时间 结束的时候再new一个; 返回的是一个精确到毫秒的时间戳。erformance.timing.navigationStart + performance.now() 约等于 Date.now()。

统计拖拽事件: 首先:为了让元素可拖动,需要使用 HTML5 draggable 属性, 分别在ondragstart,ondragend前后添加performance.now();

统计组件的加载时间: 使用 useEffect, useEffect会返回一个函数,在返回的函数中记录endtime;

使用 @web-vitals 获取性能数据 :web-vitals 是 Google 提供的一款工具,支持采集核心 Web 性能指标(如 INP、LCP、CLS 等),引入进来,放在我们页面初始化逻辑完成之后,'DOMContentLoaded’之后调用getWebVitals(‘design’)。并且使用了requestIdleCallback 或其 polyfill,它确保不会阻塞页面的渲染。

export const requestIdleCallbackPolyfill =
  window.requestIdleCallback ||  // 如果原生支持 requestIdleCallback,则使用它
  function (cb, option?: { timeout: number }) {  // 否则提供一个替代实现
    const start = Date.now();  // 记录当前时间

    return setTimeout(function () {  // 使用 setTimeout 模拟空闲回调
      cb({
        didTimeout: false,  // 标识是否因为超时执行
        timeRemaining: function () {  // 模拟 timeRemaining 方法
          return Math.max(0, 50 - (Date.now() - start));  // 返回剩余时间,最多 50ms
        }
      })
    }, option?.timeout || 1);  // 默认延迟 1ms,可以通过 `timeout` 参数调整
  };
兼容性:首先检查浏览器是否支持原生的 requestIdleCallback,如果支持则使用原生的 API,否则使用模拟的 polyfill 代码来模拟空闲回调的行为。
模拟空闲回调:通过 setTimeout 来模拟浏览器空闲时执行回调的行为。
模拟 timeRemaining:通过计算回调执行时的时间差,返回剩余的空闲时间。
超时支持:通过设置 timeout 参数来控制回调的超时时间。

为什么墨刀选择INP为监测重点?
这对于墨刀这类设计与原型工具尤为重要。墨刀的核心是帮助用户快速、流畅地进行交互式原型设计,而这种交互性非常依赖于页面或应用的响应速度。

INP(最长交互响应耗时) INP 衡量的是从用户交互到页面视觉更新的最大延迟,这直接影响用户的感知体验。 指标分布来看来看 40% 的用户交互体验有待提升。
从用户画布使用的组件数量来看,40% 的用户的画布使用的组件数量 > 400,10% 的用户组件数量 > 1200,而使用组件越多的用户越是我们核心目标用户,从两者相关联来看,越是核心用户可能对我们产品的使用体验越差。
Good|| Needs Improved || Poor (200ms 、 200-500ms、 500ms): 目标:Web Vitals 的目标是尽可能将 INP 控制在 200ms 以下,以确保用户的交互体验顺畅。

竞品对比

摹客的项目渲染与墨刀底层一致,都是使用 DOM 来渲染组件,用的也是 React,在 DOM 数量相近的情况下,类似场景墨刀的交互流畅度(FPS)更低,CPU 占用率更高,说明墨刀当前在交互性能方面还有较大的提升空间。相较于 Figma 的 webgL 渲染,在 5000 矩形的测试场景下,性能相差并不明显,但当节点数量更多时,webgl 的性能表现更佳。 50 个页面,5000 矩形组件
performance工具的使用

优化目标和指标衡量

INP在2024年3月已经取代了FIP;FID(First Input Delay,首次输入延迟)虽然衡量了用户第一次交互时的响应,但它并没有考虑后续交互的延迟,尤其是在多次交互后。
LCP (Largest Contentful Paint) 最大内容渲染时间 动态变化 这个时间通常从页面开始加载到最大内容元素显示完成。 高质量的 LCP 是在 2.5 秒以内
INP 下次互动延迟时间 Interaction to Next Paint 衡量的是从用户交互(如点击、键盘输入或触摸)到页面下一次视觉更新(绘制)所经历的最大延迟。它通过衡量所有交互的延迟来反映页面响应性。
CLS (Cumulative Layout Shift) 累计布局位移指数 动态变化 CLS 值应该小于 0.1。特别是在涉及动态内容加载、广告、字体变化或异步加载的资源时。页面内容可能会在加载过程中出现意外的视觉位移。 CLS 衡量页面在加载过程中所有不可预期的布局变化(如元素的跳动或重排)的累积值。
FCP (First Contentful Paint) 首次内容渲染时间: FCP 衡量的是浏览器渲染页面的第一个有意义内容(如文本、图像等)所需的时间。理想的 FCP 时间应小于 1.8 秒
FID 首次输入延迟时间 理想的 FID 应该小于 100 毫秒 用户首次与页面交互(如点击按钮、链接或输入字段)到浏览器响应该交互所需的时间。
TTFB (Time to First Byte) 首字节时间 首字节时间 TTFB 衡量的是从用户发出请求到浏览器接收到服务器响应的第一个字节所需的时间。理想的 TTFB 应该小于 200 毫秒,以确保页面能够快速开始加载。

核心目标: 复杂用户项目编辑时会的整体交互流畅度 FPS 超过 30。

FPS帧率统计?

绿色代表该帧正常,黄色表示丢帧
在这里插入图片描述

设备刷新率。设备刷新率是设备屏幕渲染的频率,通俗一点就是,把屏幕当作墙,设备刷新率就是多久重新粉刷一次墙面。基本我们平常接触的设备,如手机、电脑,它们的默认刷新频率都是 60FPS,也就是屏幕在 1s 内渲染 60次,约 16.7ms 渲染一次屏幕。这就意味着,我们的浏览器最佳的渲染性能就是所有的操作在一帧 16.7ms 内完成,能否做到一帧内完成直接决定着渲染性,影响用户交互。
在一个标准帧渲染时间 16.7ms之内,浏览器需要完成 Main 线程的操作,并 commit 给 Compositor 进程。丢帧:主线程里操作太多,耗时长,commit 的时间被推迟,浏览器来不及将页面 draw 到屏幕,这就丢失了一帧
在这里插入图片描述
所谓的页面卡顿、首屏加载慢,根本原因都是执行长任务,使得页面的渲染时机推后,在每一帧里得不到渲染,从而造成用户的不好体验。那么就需要知道浏览器渲染过程中的每一帧都干了些啥任务,是啥原因导致渲染时机推后,这个时候我们就需要借助浏览器性能检测工具 Performance 来进行分析,然后再做针对性的优化。

帧率的统计?

  1. 谷歌更多工具-渲染-帧渲染统计信息: 绘制帧的吞吐量、丢帧分布和GPU内存;
  2. requestAnimationFrame 会根据浏览器的刷新频率来调用回调函数,通常是 60Hz,即每秒 60 帧。因此,它能与浏览器的渲染同步,减少不必要的计算,确保动画平滑流畅。
    FPS: 每秒有几帧: FPS = 1000/ timeInterval 是当前帧和上一帧之间的时间差
    1.记录上一帧的时间(在每一帧的回调函数中记录当前的时间戳) 2. 计算时间间隔 3.计算 FPS 4.重复执行使用 requestAnimationFrame 来递归调用回调函数,从而在每一帧都进行 FPS 计算。
    3.performance.getEntriesByType(‘frame’) 返回一个包含帧信息的数组 ,每个帧对象记录了当前帧的详细时间信息,比如开始时间、渲染时间、合成时间等。
    每一帧的时间可以通过获取相邻两帧的时间戳差来计算。
    渲染时间(rendering time),即主线程的执行时间。
    合成时间(compositing time),即合成线程所花费的时间。
    FPS: 根据 frame 的 startTime 和 duration 属性
  3. performance.now()返回一个精确到毫秒的时间戳, 表示从performance.timing.navigationStart到当前时刻的时间。通过在每一帧记录这个时间戳,并计算相邻两帧之间的时间间隔,就可以得到每帧的渲染时间。然后,用 1000 除以每帧的渲染时间,就可以得到当前的 FPS。
let lastTime = performance.now();
let frameCount = 0;
const fpsInterval = 1000;
let fps = 0;

function updateFPS() {
  const now = performance.now();
  frameCount++;
  if (now - lastTime >= fpsInterval) {
    fps = Math.round(frameCount * 1000 / (now - lastTime));
    frameCount = 0;
    lastTime = now;
  }
  requestAnimationFrame(updateFPS);
}

requestAnimationFrame(updateFPS);

// 可以在需要获取FPS的地方访问fps变量
setInterval(() => {
  console.log('FPS:', fps);
}, 1000);

使用 requestAnimationFrame (updateFPS) 的原因 :requestAnimationFrame是浏览器提供的专门用于动画和渲染相关任务的 API。它会在浏览器下一次重绘之前调用传入的回调函数,通常与屏幕的刷新率同步。
**使用 setInterval 每隔 1 秒打印一次当前的 FPS 的原因 ** setInterval:以固定的时间间隔重复执行任务,一旦启动,会按照设定的时间间隔持续稳定地触发回调函数。setTimeout:只在指定的延迟时间后执行一次回调函数。代码简洁性和可维护性。

setInterval的执行频率相对固定,只要浏览器主线程没有被长时间阻塞,它会按照设定的时间间隔持续稳定地触发回调函数。而setTimeout的执行时机完全取决于设定的延迟时间,一旦延迟时间到达,就会将回调函数添加到任务队列中等待执行,但是如果主线程一直处于忙碌状态,可能会导致setTimeout的回调函数延迟执行,无法保证精确的执行时间。

如何定位问题?
比如有个task执行了Animation Frame Fired 方法,打开里面可以发现 90% 的时间是花费在 Layout,在右边可以进入源码,读取offsetTop会触发回流重绘。我们墨刀项目里面很多组件,要计算实时的偏移量然后打到组件的属性里面。

为什么频繁的回流重绘会导致卡顿?
计算复杂度: 回流涉及到重新计算元素的位置和几何属性,这可能需要遍历整个DOM树,并重新计算样式。这个计算过程比较复杂,尤其是在大型、复杂的页面上。
渲染的停顿: 当发生回流时,浏览器可能需要停止渲染,重新计算布局,然后再重新绘制,这可能导致页面的停顿或闪烁。
频繁触发: 如果在用户与页面交互的过程中频繁地触发回流和重绘,可能会导致性能问题。比如,在滚动页面时,如果频繁改变元素的样式,可能会引起多次回流和重绘,从而影响流畅度。
也就是说,频繁的回流重绘可以看做是耗时严重的任务,阻碍了页面的渲染,从而导致卡顿!

为什么读取 offsetTop 属性会触发回流重绘?
这与浏览器的优化机制有关:由于每次回流与重绘都会带来额外的计算消耗,为了优化这个过程,大多数浏览器采用了队列化修改并批量执行的策略。浏览器会将修改操作添加到队列中,直至一定时间段过去或操作达到阈值时,才会清空队列。
然而,当需要获取布局信息时,浏览器会强制刷新队列。这意味着,当你读取元素的布局信息如 offsetTop、offsetLeft 等时,需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流和重绘操作以返回正确的值。

使用 style.top 属性取代 offsetTop 即可优化?
offsetTop: 是一个只读属性 访问 offsetTop 会触发浏览器去重新计算元素的布局,因为它必须确保返回的值是最新的。这会导致「强制同步布局」,也就是说,浏览器不得不先完成所有的布局计算,甚至触发「重排」操作来确保返回准确的数值。当页面上有很多元素时,频繁访问 offsetTop 会导致性能问题,因为每次读取都会引发「重排」。
style.top:一个纯粹的样式属性 代表元素的 top 值,不会触发重排或者强制同步布局。它只修改元素的渲染,而不重新计算页面的布局。因此,使用 style.top 仅涉及「重绘」操作,性能消耗远小于 offsetTop。

利用定位改变left的位置进行移动的,这样会造成重排。我们可以利用transform代替,减少重排。

谷歌帧率插件怎么做?

  1. 首先,我们需要创建一个新的 Google 插件项目。打开你的浏览器,在地址栏中输入 chrome://extensions/ 并回车。在扩展程序页面的右上角,点击“开发者模式”按钮。然后,点击左上角的“加载已解压的扩展程序”按钮,选择一个空文件夹作为项目的根目录。
  2. manifest.json : 首先,它告诉 Chrome 浏览器如何加载和运行插件。当您加载一个插件时,Chrome 会查找 manifest.json 文件并根据其中的信息确定插件的名称、版本号、描述等基本信息。然后,Chrome 会检查权限列表并确定插件是否有足够的权限来执行所需的任务。最后,Chrome 根据指示加载所需的脚本文件和图标,并将其添加到浏览器中。 在 manifest.json 文件中不能包含注释。

怎么统计长任务?

在 JavaScript 中,可以使用 Performance API 中的 PerformanceObserver 来监视和统计长任务(Long Task)。长任务是指那些执行时间超过 50 毫秒的任务,这些任务可能会阻塞主线程,影响页面的交互性和流畅性。

  1. 火焰图 ,主要在 Main 面板中,是我们分析具体函数耗时最常看的面板,首先,面板中会有很多的 Task,如果是耗时长的 Task,其右上角会标红色三角形,这个时候,我们可以选中标红的 Task,然后放大,看其具体的耗时点。放大后,这里可以看到都在做哪些操作,哪些函数耗时了多少。然后我们点击一下某个函数,在面板最下面,就会出现代码的信息,是哪个函数,耗时多少,在哪个文件上的第几行等。这样我们就很方便地定位到耗时函数了,prototypeClone函数占比较大, 可以查看下这个函数的作用是什么? 看是否有优化空间, 通过点击Summary里的代码位置。
  2. 创建一个PerformanceObserver 对象来订阅长任务。每当检测到长任务时,它会向回调函数传递一个包含长任务性能条目的列表。在这个回调中,可以统计长任务的次数和总耗时。
// 创建一个性能观察者实例来订阅长任务
let observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
 console.log(`Task Start Time: ${entry.startTime}, Duration: ${entry.duration}`);
}
});
// 开始观察长任务
observer.observe({ entryTypes: ["longtask"] });
// 启动长任务统计数据的变量
let longTaskCount = 0;
let totalLongTaskTime = 0;
// 更新之前的性能观察者实例,以增加统计逻辑
observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
 longTaskCount++; // 统计长任务次数
 totalLongTaskTime += entry.duration; // 累加长任务总耗时
 // 可以在这里添加其他逻辑,比如记录长任务发生的具体时间等
});
});
// 再次开始观察长任务
observer.observe({ entryTypes: ["longtask"] });

或者:

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(`Long Task detected: ${entry.name}, Duration: ${entry.duration}`);
  });
});

observer.observe({ entryTypes: ['longtask'] });

怎么优化长任务?

  1. 拆分长任务 :使用 setTimeout 在任务之间插入间隙。
    表格渲染: 渲染大型表格或列表(如 10,000 行数据)时,分批添加数据到 DOM。
const data = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
const container = document.getElementById('list-container');

function renderListChunk() {
  const chunkSize = 100; // 每次渲染 100 条
  let index = 0;

  function renderChunk() {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < chunkSize && index < data.length; i++) {
      const li = document.createElement('li');
      li.textContent = data[index++];
      fragment.appendChild(li);
    }
    container.appendChild(fragment);

    // 如果还有剩余任务,继续执行
    if (index < data.length) {
      setTimeout(renderChunk, 0);
    }
  }

  renderChunk();
}

renderListChunk();

无限滚动(Infinite Scroll): 当用户滚动页面时,分批加载和渲染数据,而不是一次性加载全部内容。
复杂动画: 逐步计算动画路径,而不是一次性生成所有帧。

2.限制并发请求 : :限制并发请求的数量,可以保证主线程有足够的时间来处理页面渲染和用户输入,避免在请求处理时阻塞 UI。 假设你需要加载一批图片,但希望一次最多只发起 3 个并发加载。

// 用于发送请求的函数
function fetchUrl(url) {
  return fetch(url)
    .then((response) => response.json())
    .catch((error) => {
      console.error('Error fetching:', url, error);
      throw error;
    });
}

// 控制并发请求的函数
function limitConcurrency(urls, maxConcurrency) {
  const results = [];
  let activeCount = 0; // 当前正在运行的请求数
  let index = 0;

  return new Promise((resolve, reject) => {
    function next() {
      if (index >= urls.length && activeCount === 0) {
        return resolve(results); // 所有任务完成
      }

      if (activeCount < maxConcurrency && index < urls.length) {
        const url = urls[index++];
        activeCount++;

        fetchUrl(url)
          .then((result) => {
            results.push(result);
            activeCount--;
            next(); // 继续执行下一个任务
          })
          .catch(reject);
      }
    }

    // 初始化并发请求
    next();
  });
}

// 请求的 URL 数组
const urls = Array.from({ length: 20 }, (_, i) => `https://jsonplaceholder.typicode.com/todos/${i + 1}`);

// 执行并发控制,每次最多 3 个请求
limitConcurrency(urls, 3).then((res) => {
  console.log(res);
}).catch((err) => {
  console.error('Error in fetching data:', err);
});

//并发请求控制:
//通过 activeCount 来限制最多同时进行 maxConcurrency 个请求。
//每个请求完成后,next() 函数会被调用,检查是否可以发起新的请求。
//直到所有请求完成,最终返回所有的结果。

limitConcurrency(urls, maxConcurrency):
这个函数负责控制并发请求。它接收两个参数:urls:需要请求的 URL 列表。maxConcurrency:最大并发数,即同时发起的请求数量。
核心逻辑:使用一个 index 变量来追踪当前要执行的任务。使用 activeCount 来控制当前正在执行的请求数量。每当一个请求完成,调用 next() 函数来决定是否启动下一个请求。当所有任务都完成时,返回 results 数组。

其他解法

  1. 减少阻塞主线程的操作
    优化 JavaScript:
    —减少 DOM 操作:批量操作 DOM,避免频繁访问和修改。
    —减少重绘和回流:合并样式更改,避免多次触发回流。
    —异步加载资源:使用 defer 和 async 标签加载 JavaScript,减少阻塞。

  2. requestAnimationFrame:专门用于动画和渲染优化,会请求浏览器在下一个重绘周期时调用传入的回调函数(此处为 animate 函数)。它根据显示刷新率来决定调用频率,通常是每秒 60 帧(即 60 FPS),从而确保动画渲染流畅。 比如定义了一个animation函数,它接收一个 time 参数,代表当前的时间戳。第一次调用时,计算动画已执行的时间,如果动画已执行的时间小于 2000 毫秒(即 2 秒),则继续执行动画;否则,动画结束。

let start = null;

function animate(time) {
  if (!start) start = time;
  const progress = time - start;

  // 动画逻辑
  if (progress < 2000) { // 动画执行 2 秒
    // 更新位置、状态或绘制
    console.log(Animating: ${progress / 1000}s);
    requestAnimationFrame(animate); // 继续执行下一个动画帧
  } else {
    console.log('Animation complete');
  }
}
requestAnimationFrame(animate); // 启动动画   解释思路 怎么优化的

requestIdleCallback:用于调度低优先级的任务,比如后台计算、数据缓存等,它会在浏览器空闲时执行任务,确保不会影响关键的渲染操作。根据这个方法来判断是否需要继续执行任务timeRemaining() ,返回剩余的空闲时间,单位是毫秒。如果这个值大于 0,说明仍有空闲时间可以继续执行任务;如果小于或等于 0,则说明空闲时间不足,应该结束当前的任务执行。
如果有一些定期任务(如自动保存、日志记录等),可以通过 requestIdleCallback 在浏览器空闲时进行处理

优化方案

浏览器分层优化

每一帧在渲染进程中,主线程会根据 DOM 树和 CSSOM 的变更计算出新的布局树(layout),然后根据元素属性分层,然后根据分层paint绘制,内存会记录。
发现墨刀原型在 DOM 数量相似的情况下,分层数量和内存占用上约为摹客的 10 倍,这也是影响交互性能的一个原因。在上千组件的复杂项目交互过程(光标移动、组件拖拽)中,会频繁触发浏览器的分层处理,渲染耗时占比 30% 以上。
哪些元素会触发新的图层?

  1. 拥有 3D 或视距转换的元素: 使用 transform: translate3d() 或 translateZ(0)。CSS 属性触发硬件加速:will-change、translate、scale 等。
  2. 有视频、canvas 或复杂动画的元素: 和 元素会自动创建独立图层。CSS 动画使用了 opacity 或 transform。
  3. 具有混合模式的元素:使用 mix-blend-mode 会触发一个新的合成层。
  4. 拥有 position: fixed 的元素: 固定定位元素会被独立成图层,以提高滚动性能。

在浏览器 devtool 中,可以查看分层的多少,以及分层原因、内存开销等信息在这里插入图片描述
渲染优化的主要目的是:合理减少大(内存暂定 > 5MB,大概尺寸为 1000 * 1000)分层的数量及绘制频次。合理的意思则是对于需要高频绘制的元素可以优先进行独立分层避免频繁触发所在大分层的重绘甚至重排,对于绘制频率低的元素如果其宽高比较大,尽量可以合并成一个图层,避免过多的内存开销。

  1. document 根节点分层 浏览器默认分层
  2. 画布中每个页面、页面的溢出线及直接在画布中的组件及都是一个独立的分层 不合理,分层过度 使用 will-change 只对选中页面独立分层
  3. 横/竖滚动轴分层 设置了will-change导致分层, 【新增】并改用 translate 代替 left/top。

更进一步的思考优化: 分层内存占用的大小与元素的 width、height 有关,却不受 scale 的影响,1000px 宽高的矩形占内存约 5 MB,如果用 100px 宽高的矩形设置 scale(10) 其内存则只有几十 KB,适合场景如:hover、select 元素高亮选框,但会带来额外的理解成本和开发成本。
图层的实际内存占用与元素的原始尺寸(width 和 height)有关,而与 scale 属性无关。
图层的内存计算公式: 图层的内存占用与元素的分辨率和颜色通道有关,公式如下:
4. 内存占用 = width × height × 色深 / 8 常见色深:32 位,即每个像素 4 字节(RGBA) 一个 1000px × 1000px 的图层:1000 × 1000 × 4 = 4,000,000 字节 ≈ 4 MB。考虑到一些边缘开销,通常被视为约 5 MB
5. 使用 transform: scale() 并不会改变元素在图层中的实际尺寸,只是将图层内容在屏幕上放大或缩小,因此不会影响图层的内存大小。
在这里插入图片描述

will-change的原理

告诉浏览器哪些属性会变,提前启用合成层。它会为该元素创建一个新的“合成层”(composite layer)。合成层通常由 GPU 来处理,这意味着该元素在变化时不需要经过整个渲染管道(如重新计算布局、重新绘制等),从而减少了 CPU 的负担,提升了动画和变换的流畅度。

注意过度使用可能导致性能问题,只在必要的时候用。 单次变换:只做一次动画或变换的元素,动画结束后元素的状态不会再发生变化。

高频交互优化

减少高频操作行为过程中 DOM 事件触发(如节流)、减少 DOM 更新操作(如减少组件不必要的渲染)、减少 DOM 节点(如按需加载)。
用户操作过程中高频触发的操作主要为:画布缩放、移动,组件/页面 hover、选中、拖拽。目前交互性能问题明显的是组件/页面 hover、拖拽。
组件/页面 hover、拖拽两个事件主要都是 mousemove 事件触发。

  1. 比如我们在App.screenContainer.document,两处 mousemove 事件监听,其中 App 中的是监听整个 window,两者逻辑处理都比较简单优化。在整个应用中,可能有多个地方都在监听 mousemove 事件,尤其是如果两个地方的逻辑处理相似或可以合并时,集中监听减少了监听器数量,降低了内存消耗。
  2. 即使是在 ScreenContainer 中集中监听 mousemove 事件,也要注意对事件进行控制。例如,可以使用 throttle 或 debounce 来限制事件触发的频率,避免过于频繁的处理逻辑导致性能问题。
  3. 在 FECanvas 中每次 mousemove 都会实例化一些类,但实际很多时候不会用到这些实例,造成 js 额外的运行损耗。按需实例化,比如鼠标移动了200再xx。 避免在 mousemove 中频繁实例化对象
  4. 富文本中有很多的层级目录。切换时,默认会展开页面下一级目录,如果图层数量多,会导致短时间渲染过多组件,明显出现卡顿。默认收起所有页面,只对选中后的页面展开 懒加载(Lazy Loading)
  5. 在处理 mousemove 事件时,频繁的 DOM 查询操作,特别是使用 querySelectorAll 和 contains 来查找和判断 DOM 关系,确实可能导致性能问题,尤其是在 DOM 节点较多时。为了提高性能,可以考虑使用 closest 方法代替 querySelectorAll + contains closest 方法可以在 DOM 树上从目标元素开始,向上遍历直到找到符合条件的祖先节点(包括自身)。它的优点是只需要在当前节点及其祖先节点中查找,而不需要遍历整个 DOM 树,性能上比 querySelectorAll + contains 更优。

组件数据优化

项目数据传递机制总结: 可以分为三层架构:flatJSON(数据源)、FolderStore(数据处理层)、Redux(状态管理与 UI 驱动层)。

每个组件有很多属性,属性的数据存储在一个树形结构的flatjson里面,然后在初始化的时候,FolderStore 在 flatJson 的基础上构建出用于展示的组件,使用map再把数据存了一遍,用于快速获取单个 node 信息,最后redux层会在 state 中引用 FolderStore 的实例,把数据传递给UI;

FolderStore 既包含树形结构,也使用了 Map 来优化数据的访问和更新。它是一个混合的数据结构,在 树形结构 和 缓存映射 之间结合,以提供高效的数据管理。

用户操作 → 触发 action → 更新 FolderStore 中的数据(通过 nodeMap 和 flatJSON)→ FolderStore 通知 Redux 更新 state

因为在 redux 中是组件树是以树形结构保存,且只有根节点对 store 状态进行了监听,所有的组件属性的变更必须经有根节点触发。
简单来讲就是数据的更新是事件触发,然后自下而上更新,UI 组件的更新是数据驱动,由上而下进行更新。
数据更新:
用户操作 → 子组件数据变更 → 父组件数据回溯更新 → 根节点数据更新

UI 渲染:
根节点数据更新 → 子组件接收新数据 → 逐级向下渲染

class FolderStore {
  constructor(flatJSON) {
    this.nodeMap = new Map();  // 缓存组件数据,快速查找
    this.root = this.buildTree(flatJSON);  // 根节点,树形结构
  }

  // 基于 flatJSON 构建树形结构
  buildTree(flatJSON) {
    const traverse = (nodeId) => {
      const node = flatJSON.find(item => item.id === nodeId);
      if (node) {
        node.children = node.children.map(traverse);  // 递归查找子节点
        this.nodeMap.set(node.id, node);  // 缓存节点
        return node;
      }
      return null;
    };

    return traverse('root');  // 从根节点开始构建树
  }

  // 获取节点的快速访问方法
  getNode(nodeId) {
    return this.nodeMap.get(nodeId);  // 使用 map 提供 O(1) 查找
  }

  // 更新节点
  updateNode(nodeId, newData) {
    const node = this.nodeMap.get(nodeId);
    if (node) {
      Object.assign(node, newData);  // 更新节点数据
      return node;
    }
    return null;
  }
}

画布数据操作的现状与问题:
1.数据跨层调用 : 组件直接调用 sdkStore.getHotItem 获取数据,而不是通过 folderStore。folderStore 的缓存未充分利用,增加了性能开销。 sdkStore.getHotItem 为 0.2ms,而 folderStore.getNode 为 0.1ms。 优化策略:统一通过 folderStore 获取组件数据,充分利用其缓存,避免跨层调用。
2. 数据更新没有收口,更新数据时逻辑过于复杂。

当前优化方案重点聚焦于数据获取规范化组件渲染性能优化

  1. 非必要渲染导致性能差 : 根节点更新时会生成新的 otherProps 对象,传递给子组件,导致所有子组件重新渲染。
    写一个isqual函数判断nextprops和prevprops是否相等,使用 React.memo 和 HOC(高阶组件)封装子组件,避免不必要的渲染。
    优化后效果:组件拖拽时:耗时从 >500ms 降低到 <80ms。 拖拽组件到新页面:耗时从 >200ms 降低到 <40m
  2. 富文本组件高度计算逻辑影响性能: 重新计算单元格内容高度来设置 overflow: hidden,受分层影响。移除内容高度计算逻辑,直接设置 overflow: hidden 样式。
  3. 拖拽富文本组件性能问题: 拖拽过程采用节流机制,拖拽数量越多,节流间隔越大,最大不超过 40ms。 限制函数的执行频率,即在一定时间间隔内只执行一次操作。
    实现步骤
    动态计算节流间隔:

根据拖拽的组件数量,动态设置节流间隔。
拖拽数量越多,节流间隔越大,但要设定一个最大值(例如:40ms)。
使用节流函数:

可以通过手写节流函数,或使用库(如 lodash.throttle)来实现。
在拖拽事件回调中,使用节流函数包装更新操作。
实现逻辑:

监听拖拽过程中的 mousemove 或 onDrag 事件。
根据拖拽数量计算节流间隔。
使用节流函数控制触发频率,避免过于频繁的 DOM 更新或状态更新。

// 自定义节流函数
function throttle(func, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 拖拽数量影响节流间隔
function getThrottleInterval(dragItemCount) {
  // 拖拽数量越多,间隔越大,最大不超过 40ms
  return Math.min(40, 10 + dragItemCount * 2);
}

优化效果

7053 个组件,5万+ dom FPS:16-> 37

影响范围和风险 思考和改进

主要影响的画布行为:
画布的交互事件,包括鼠标移动高亮相应组件、鼠标点击选中组件、鼠标拖动移动组件、鼠标框选选中多个组件、鼠标滚轮画布缩放。
后续改进:

  1. 数据分层的优化
  2. 数据处理使用 webworker
  3. 渲染器抽离并支持 webgl 渲染

前端防止劣化?

  1. 建立性能基准 :将性能基准作为代码评审和 CI/CD 流程中的一部分。
  2. 创建独立的优化分支 :确保主分支的稳定性。通过拉取最新代码定期同步分支,避免产生大的代码冲突。
  3. 团队性能意识培训 : 定期对团队成员进行性能优化培训,分享最新性能优化技术和案例。让开发人员了解性能优化的重要性及常见问题解决方法。 设立奖励机制,鼓励团队成员在开发过程中主动关注性能优化,对提出有效性能优化方案的成员给予奖励。
  4. 监控生产环境性能 : 使用sentry 持续跟踪生产环境的性能数据,定期回顾性能报告,发现和解决潜在问题。
  5. 代码审查与规范 : 需求迭代时,在代码审查环节重点关注可能影响性能的代码改动。如新增的 DOM 操作是否频繁引起重排重绘,新的 JavaScript 函数是否存在复杂计算阻塞主线程。
  6. 制定性能规范 :团队制定前端性能编码规范,如限制 CSS 选择器深度、避免在循环中操作 DOM 等

设计规范

高效的UI组件库,怎么定义高效?

  1. 轻量化设计 :组件库代码去除冗余,只保留核心功能代码。例如,基础按钮组件,仅包含样式渲染、点击交互等必要代码,避免复杂逻辑堆积。
  2. 开发高效 : 简洁 API ; 丰富文档与示例;包含组件功能介绍、属性说明、事件回调等。如
  3. 维护高效 :模块划分,代码规范, 版本控制:通过语义化版本号管理组件库版本,明确不同版本更新内容和兼容性。如1.0.0版本,1代表大版本更新,可能有不兼容变化;0表示小版本更新,增加功能但保持兼容;0表示补丁版本,修复 Bug。

1.为什么要重写并整合 6 大类组件及原组件存在的主要问题?

  1. 代码混乱与维护困难: 随着业务的发展,原有组件的代码可能变得越来越复杂,不同开发人员的代码风格差异大,导致代码可读性和可维护性差。例如,一个简单的按钮组件可能在不同页面有不同的实现方式,修改一处功能需要在多个地方进行调整,增加了开发成本和出错风险。
  2. 功能扩展受限: 业务需求不断变化,原组件在设计时可能没有充分考虑到后续的功能扩展,导致在添加新功能或特性时,需要对组件进行大规模的修改甚至重写。比如,当需要在 modal 组件中添加新的交互效果或内容展示方式时,原有的代码结构难以支持,只能通过 hack 的方式实现,影响了组件的稳定性和可扩展性。
  3. 样式不统一: 在长期的开发过程中,不同页面或模块对组件的样式要求可能存在差异,导致组件在不同场景下的样式不一致,影响了产品的整体视觉效果和用户体验。例如,inputNumber 组件在不同表单中的字体大小、颜色、边框样式等可能各不相同,给用户一种不专业、不规范的感觉。
  4. 性能问题: 随着用户量的增加和页面复杂度的提高,原组件可能存在性能瓶颈,如渲染速度慢、资源占用过多等。例如,高级图表组件在处理大量数据时,可能会出现卡顿现象,影响用户对数据的查看和分析

2.在重写组件过程中,怎么思考?

在这里插入图片描述
我们可以把功能或者需求类似的有机体封装成一个业务组件,并对外暴露接口来实现灵活的可定制性,这样的话我们就可以再不同页面不同子系统中复用同样的逻辑和功能了。
如果要写一个 Modal 弹窗组件,需要从以下多个方面进行考虑:
在这里插入图片描述

  1. 功能需求: 能够在页面上弹出一个覆盖层,展示自定义的内容,如文本、图片、表单等。内容区域应支持滚动,以适应较多内容的展示。如右上角的 “×” 按钮、点击遮罩层关闭等。添加淡入淡出、滑动等动画效果,使弹窗的显示和隐藏更加平滑自然,提升用户体验。
  2. 交互设计: 当弹窗显示时,应阻止页面其他部分的交互,确保用户专注于弹窗内容。支持键盘操作,如按下 Esc 键关闭弹窗,通过 Tab 键在弹窗内的元素间切换焦点等。
  3. 组件设计原则
    **单一职责原则:**确保每个组件只负责一项明确的功能,例如 button 组件只负责处理按钮的点击、样式等基本功能,而不涉及与按钮无关的业务逻辑,这样可以提高组件的内聚性,便于扩展和复用。

单一职责原则定义:一个类或模块应该只有一个引起它变化的原因,即一个类或模块只负责一项职责。
可配置性原则:通过 props 或配置文件等方式,让组件的行为和样式可以灵活配置。比如,modal 组件可以通过配置参数来决定弹出框的大小、位置、是否可拖动等,满足不同业务场景的需求。 visible、title、
4. 事件与回调
生命周期事件:提供弹窗的生命周期事件,如打开前、打开后、关闭前、关闭后等,方便外部代码在这些关键时刻进行相应的处理。
回调函数:支持传入回调函数,如点击按钮的回调、关闭弹窗的回调等,以便与外部业务逻辑进行交互和集成。
5. 性能优化
渲染性能:优化弹窗的渲染性能,避免在弹窗显示时出现卡顿或加载缓慢的情况。可以采用懒加载、虚拟列表等技术,提高渲染效率。
内存管理:在弹窗关闭后,及时清理相关的 DOM 节点和事件监听器,避免内存泄漏,确保页面的性能稳定。

在开发 UI 组件库时,以下设计模式有助于简化开发并提升稳定性:

  1. 单例模式
    应用场景:适用于组件库中某些全局唯一的资源或状态管理。例如,组件库可能有一个全局的主题配置对象,无论在多少个组件中调用,都应是同一个实例,确保主题配置的一致性。
    优势:避免重复创建全局资源,节省内存,提升稳定性。同时,提供单一的访问点,便于集中管理和修改全局状态。
  2. 工厂模式
    应用场景:当创建组件实例的过程较为复杂,或需要根据不同条件创建不同类型的组件时,可使用工厂模式。比如在创建表单组件时,根据表单类型(登录表单、注册表单等),工厂函数可以返回不同预配置的表单组件实例。
    优势:将组件的创建逻辑封装在工厂函数中,组件使用者只需调用工厂函数获取组件实例,无需关心复杂的创建过程,简化开发。同时,若创建逻辑发生变化,只需在工厂函数内修改,不影响组件的使用,提升组件库的可维护性和稳定性。
  3. 组合模式
    应用场景:用于构建具有树形结构的组件,如菜单组件、树形结构组件等。将简单组件组合成复杂组件,每个组件既可以是叶子节点(简单组件),也可以是包含其他组件的容器节点。
    优势:使得组件的层次结构更清晰,易于扩展和维护。通过统一的接口操作组合体和单个组件,简化开发逻辑。例如,在菜单组件中,无论是单个菜单项还是整个菜单树,都可以用相同的方式进行操作和管理。
  4. 装饰器模式
    应用场景:当需要为组件添加额外功能,同时又不希望修改组件原有代码时,装饰器模式非常有用。比如为按钮组件添加加载动画效果,通过装饰器可以在不改变按钮核心代码的情况下,为其增加加载状态的显示功能。
    优势:符合开闭原则,即对扩展开放,对修改关闭。

3.有提到渲染性能,这个具体怎么做的?

图片懒加载:
如果弹窗内包含较多图片,尤其是当图片尺寸较大或者数量较多时,可采用图片懒加载技术。在 HTML 中,将 标签的 src 属性设置为空,添加一个自定义属性(比如 data-src)来存放真实的图片地址。例如:

<img data-src="https://example.com/large-image.jpg" alt="示例图片" />

然后通过 JavaScript 监听滚动事件(或者使用一些成熟的懒加载库,如 lazysizes),当图片进入可视区域时,再将 data-src 中的地址赋值给 src 属性,触发图片的加载。代码示例如下:

const lazyImages = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
        }
    });
});
lazyImages.forEach(img => observer.observe(img));

组件懒加载:
对于弹窗内复杂的组件或者模块,特别是那些可能不是立即需要展示给用户的内容,也可以进行懒加载。如果使用 React 框架,可以借助 React.lazy() 和 Suspense 来实现。例如,假设有一个复杂的图表组件在弹窗内,代码如下:

const ChartComponent = React.lazy(() => import('./ChartComponent'));
function Modal() {
    return (
        <div className="modal">
            {/* 其他弹窗内容 */}
            <Suspense fallback={<div>加载中...</div>}>
                <ChartComponent />
            </Suspense>
        </div>
    );
}

当弹窗渲染到 ChartComponent 所在位置时,如果它还未加载,会先显示 fallback 中的加载提示内容,然后再异步加载组件并渲染,避免在弹窗初始化时就加载所有组件导致性能开销过大。

内存管理
清理DOM节点,移除事件监听器。 当弹窗关闭时,首先要遍历弹窗内所有添加了事件监听器的元素(如按钮的 click 事件、输入框的 input 事件等),并使用相应的 removeEventListener 方法移除这些监听器。
删除DOM元素: 获取弹窗对应的 DOM 节点(通常是最外层的模态框容器元素),使用 parentNode.removeChild() 方法将其从父节点(一般是 document.body)中移除,彻底删除该 DOM 结构,释放其所占用的内存空间。

4.现在回头看,觉得当时设计规范哪里有做的不太好的地方?提升 ?

参数暴露过多,自由度过高:封装的modal有大约30多个参数,对于别人来说其实是黑盒,但是别人不一定都要用这么多个。

响应式设计方面:

断点设置不够灵活: 原设计规范中设定的响应式断点在一些特殊设备或屏幕尺寸下不能很好地适配。例如,toast要求对于一些介于常规断点之间的小众设备屏幕,页面布局可能会出现显示异常,如元素挤压、换行不自然等。
组件响应式行为不一致:部分组件在不同屏幕尺寸下的响应式行为不够一致,有的组件能够自适应调整布局和样式,而有的组件则需要额外的手动调整。这增加了开发人员在处理响应式布局时的工作量和复杂性。

文档与沟通方面:
设计规范文档不够详尽: 文档对于一些组件的使用方法、属性说明以及特殊情况的处理不够详细,导致开发人员在使用过程中需要不断地进行试错和猜测。例如,对于一些组件的高级配置选项,文档中没有提供足够的示例和解释。
跨团队沟通协作机制不完善: 在设计规范的实施过程中,设计团队与开发团队、运营团队等之间的沟通协作存在一些障碍。例如,设计团队对设计规范的更新不能及时传达给开发团队,导致开发与设计出现脱节。

改进:
modal组件后来进行了一版优化,对接口进行收束,对于基础的参数 使用modalcommon 对于所有参数的使用 改为了modalpro 控制组件的自由度

接口收束的优势
降低使用门槛与学习成本:之前接口繁杂时,开发人员要花费大量时间去熟悉不同参数的用途与规则,容易出错。划分出 modalcommon 用于基础参数传递,让开发者能迅速上手,聚焦于最常用、最核心的功能配置,快速搭建模态弹窗的基本框架。例如,只需传入标题、内容文本这类基础信息,就能快速展示一个简约的提示弹窗,节省时间成本。
提高代码可读性与可维护性:统一基础参数至 modalcommon,项目代码中频繁出现的模态弹窗实例变得规整。维护代码时,开发人员一眼就能明晰弹窗的关键设定;后续交接项目或排查问题,也能快速定位,降低理解成本。
使用 modalpro 调整自由度的好处
**适配复杂场景需求:**业务发展中,总会遇到特殊、复杂的弹窗需求,像是带多层嵌套表单、实时预览功能的弹窗。modalpro 囊括所有参数,赋予开发者充分自由度,可按需精细调整弹窗的样式、交互细节、数据绑定规则等。例如,电商项目的商品详情弹窗,借助 modalpro 调整图片展示尺寸、添加商品规格切换交互,贴合业务特性。
灵活性与扩展性兼得:开发新产品功能、迭代旧功能时,modalpro 避免了接口限制导致的功能施展不开问题。团队可以基于现有 modalpro 接口拓展自定义属性、方法,引入新交互效果,为产品升级留出足够空间,跟上业务创新节奏。

总体而言,这次 modal 组件的接口优化利于当下项目开发效率提升,也为后续产品迭代、团队协作夯实基础,只要妥善处理好后续事宜,就能收获长久效益。功在当代,立在千秋。

5.你这个modal弹窗都有什么功能?怎么做的?

艰难的整合
墨刀在长期的发展过程中,逐渐积累了多种不同类型的 Modal 弹窗实现,像 StyleModal、core - modal、animationmodal 等。各个业务仓库为了适配自身业务需求,又分别在这些已有弹窗基础上进行了不同程度的封装,导致整个系统中弹窗相关的结构变得极为复杂。
以原型业务为例,在 worksplace 中进行了独特的封装处理;而在平台侧,甚至只是针对某些 Modal 的 Modalhead 部分进行封装,并且部分弹窗还涉及到像 shouldrenderFooter 这样特定的逻辑判断,用于决定是否展示底部相关内容。这种各自为政且多层封装的情况,使得代码的维护成本日益增高,理解和扩展弹窗功能也变得越发困难,不利于整个项目的持续迭代与协同开发。

庖丁解牛,分而治之
为了解决上述复杂混乱的局面,我们采取了整合策略。把多个不同的弹窗整合成一个统一的基础弹窗,并将其放置在 mb - component 组件库中。这样做的好处是,当具体的业务仓库需要使用弹窗组件时,只需要从这个统一的组件库中引入基础弹窗,然后按照自身业务特点进行一次针对性的封装,最后再引入到更具体的业务组件内使用,就能够满足多样化的业务场景需求。
这个基础弹窗根据功能和使用场景的不同,进一步细分为了几个版本:

基础版: 主要适用于相对简单、通用的场景,比如邀请弹窗、提示弹窗等。这类弹窗通常只需要展示简单的文本信息,告知用户某些事项或者引导用户进行一些基本操作,功能和样式相对简洁明了。
加强版: 针对一些涉及到多步骤操作或者有更多交互需求的业务场景,像交接权限、发票弹窗等就归为此类。加强版弹窗除了展示必要的信息外,可能还会包含多个操作按钮、表单元素等,以满足更复杂的业务逻辑处理。
自定义版: 为那些功能高度定制化的业务场景而设计,例如支付弹窗、优惠券弹窗、登录弹窗等。这些弹窗往往需要根据具体的业务规则,在界面布局、交互方式、数据验证等多个方面进行深度定制,以提供贴合业务的完美用户体验。
新旧兼容
考虑到原始组件中弹窗已经有多达 200 次的引用,为了避免大规模修改现有代码导致潜在的风险以及减少对业务的影响,我们采取了新旧兼容的方式。在整合过程中,保留了老的参数写法,让原有的代码能够继续正常运行。同时,逐步引入新的参数形式,在新的开发或者对旧功能进行迭代时,鼓励使用新参数,使得新参数写法能够逐渐取代老的写法,实现平稳过渡,保障整个项目在弹窗组件使用上既能延续之前的功能,又能朝着更规范、更易于维护的方向发展。

技术实现一个简单的 modal
在技术实现层面,对于一个简单的 Modal 组件,采用了如下方式:
渲染机制: 利用createPortal这一特性,将作为弹窗内容的children渲染在它所创建的一个独立的div元素中。**createPortal允许我们将子节点渲染到父组件的 DOM 层次结构之外的 DOM 节点中,**在这里就是将弹窗内容挂载到更合适的位置,便于进行样式控制和避免一些层级相关的问题。
挂载与卸载控制: 通过监测open参数的变化来决定该div元素在document.body上的挂载与卸载操作。 当open参数为true时,表示弹窗需要显示,此时将承载弹窗内容的div元素挂载到document.body上,使其在页面中呈现出来;而当open参数变为false时,意味着弹窗要关闭。onCancel afterClose
总结
从最后的代码提交情况来看,这次对 Modal 弹窗的整合工作实际上是在做减法。通过梳理和整合,删掉了很多冗余的代码, 无论是在样式层面,还是结构层面,都使得代码更加简洁、清晰。同时,在整合过程中也参考了 Ant Design 等优秀的组件库在 Modal 组件设计方面的一些理念和实践经验,汲取其优点,让我们的 Modal 弹窗组件在功能、易用性以及可维护性等方面都得到了显著的提升。

手把手实现一个modal

6.参考了ant Design的组件库,有看过源码吗?觉得它哪里好?

层次结构合理:Button 组件拆分成多个子组件或内部函数,每个部分负责特定的功能,如按钮的渲染、状态管理、样式应用等。通过这种方式,降低了代码的耦合度,各个部分可以独立开发和测试。
浏览器兼容性处理: 在源码中,对不同浏览器的兼容性进行了处理,确保按钮在各种主流浏览器中都能正常显示和使用。通过使用浏览器前缀、特性检测等技术,解决了浏览器兼容性问题。
可访问性优化: 注重可访问性 ,为按钮添加了必要的aria- 属性,如aria-label、aria-disabled等,使得屏幕阅读器等辅助技术能够更好地识别和操作按钮,提高了产品的可访问性。

7. InputNumber 组件亮点分析

  1. 多样化属性控制: 组件接收众多属性,像 step(控制增减步长)、precision(数值精度)、min(最小值)、max(最大值)等
  2. 格式化与解析能力: 通过 formatter 和 parser 这两个属性对应的函数,能够灵活地对输入值进行格式化显示以及解析处理。比如可以将输入的数值格式化为带有特定单位(如 formatter 中添加货币符号等)的展示形式。
  3. 多种输入方式支持:既支持常规的键盘输入修改数值,又提供了按钮点击(增减按钮)以及鼠标拖动标题栏(通过 handleRectResize 实现类似滑块调整数值的功能)等交互方式来改变数值,给用户提供了多样化、便捷的操作体验。
  4. 长按递增 / 递减逻辑:实现了长按增减按钮后按照一定时间间隔自动递增或递减数值的功能。
  5. 严谨的数值验证:在 checkValidity 和 correctNumber 等方法中,对用户输入的数值进行合法性验证以及纠正处理,用户输入超出 min 和 max 范围的值时,组件能自动纠正为边界值。
  6. 通用的数值输入场景适用:整体设计上没有过多绑定特定业务逻辑,只要是涉及数值输入的场景,通过配置不同的属性,都可以复用该组件,在不同的项目或者页面模块中发挥作用,减少了重复开发成本。

8.请简述InputNumber 组件中是如何处理数值的合法性验证的,涉及哪些关键方法和逻辑?

通过 checkValidity 和 correctNumber 这两个方法来实现。

  • checkValidity 方法逻辑:
    首先会判断一些特殊情况,比如输入的是 + 号时,会根据组件配置的 max 值是否大于 0来确定其合法性;输入 - 号时,依据 min 值是否小于 0来验证。对于常规数值,会检查是否是有限值(isFinite 函数判断),并且通过 correctNumber 方法纠正后的数值是否和输入的数值相等。
  • LcorrectNumber 方法逻辑:
    根据组件的 min、max、precision 等属性以及传入的数值和后缀(通过 parser 解析出来),先将传入的数值解析后进行取值范围的限制(通过 Math.min 和 Math.max 与 min、max 比较),然后按照 precision 精度进行 toFixed 处理,最终返回纠正后的合法数值。通过这两个方法配合保证用户输入的数值始终符合组件设定的规则。

9.在 InputNumber 组件里,长按增减按钮实现连续数值变化的功能是如何实现的?

事件绑定与初始处理:在 onMouseDown方法中,首先通过 e.nativeEvent.stopPropagation() 阻止事件冒泡,然后调用 handleBeforeChangeValue 进行数值变化前的一些准备工作(涉及的业务逻辑前置处理)。接着获取当前输入框中的数值(通过 parser 方法解析),根据按键事件(结合 getStep 方法根据 shiftKey、metaKey 等判断步长倍数)确定增减的步长 step。之后通过 setConfirmedValue 方法设置确认后的数值(这里会先调用 correctNumber 进行数值纠正确保合法性),并调用 focusOnInput 方法确保输入框保持焦点状态。
长按逻辑触发:重点在于设置了定时器 longPressedTimeout,当长按时间达到 定义为 500 毫秒后,会启动另一个定时器 steppingInterval,其时间间隔为 LONG_PRESSED_STEPPING_INTERVAL(30 毫秒),在这个定时器的回调函数中,不断重复执行类似 onStep 里的数值更新操作(即重新获取当前数值、计算并设置新的确认数值),从而实现了每隔一定时间自动递增或递减数值的连续变化效果。
结束处理: 在 onRelease 方法中,通过 clearTimeout 和 clearInterval 分别清除 longPressedTimeout 和 steppingInterval 这两个定时器,停止连续数值变化,并调用 handleAfterChangeValue 进行数值变化完成后的后续处理。

10.从复用性和可扩展性角度来看,InputNumber 组件有哪些设计特点?

无强业务绑定:组件整体的代码结构并没有紧密耦合特定的业务逻辑,它专注于数值输入和相关交互功能的实现,使得在各种涉及数值输入的项目或者页面模块中都能方便地使用。
预留扩展接口: 组件提供了如 optionList 属性以及相关的事件回调(如 onSelect),这为与其他组件结合扩展功能留出了接口。

11.colortoken怎么保证项目颜色是一致的?

  • 建立统一的颜色规范:
    制定一套完整的 colorToken 规范,明确每种颜色的用途和语义,如主色调、次色调、警告色、成功色等,并为每种颜色定义一个唯一的名称和对应的色值。例如,定义 “primaryColor” 为产品的主色调,其色值为 “#007BFF”,在整个产品中,所有表示主要操作按钮、重要信息等的元素都使用该颜色。
  • 使用变量或函数管理颜色:
    -在 CSS 或 JavaScript 中,使用变量或函数来存储和获取颜色值,而不是直接在代码中硬编码颜色。例如,在 CSS 中使用 CSS 变量::root { --primary-color: #007BFF; },然后在组件的样式中通过color: var(–primary-color);来引用颜色,这样当需要修改颜色时,只需要修改变量的值,而不需要在每个使用该颜色的地方进行修改。
  • 颜色主题切换功能:
    -考虑到我们的产品需要支持不同的主题在整合 colorToken 时,可以设计颜色主题切换功能。通过定义多个颜色主题对象,每个主题对象包含一套完整的颜色值,根据用户的选择或系统设置动态切换主题。例如,在 JavaScript 中定义一个主题对象:const lightTheme = { primaryColor: ‘#007BFF’, secondaryColor: ‘#6C757D’ }; const darkTheme = { primaryColor: ‘#FFFFFF’, secondaryColor: ‘#343A40’ };,然后根据用户的主题选择来应用相应的颜色。

12. 项目接入了CI/CD?

1. Git 提交
开发人员在本地完成代码的编写、修改以及相关功能的测试后,会将代码提交到对应的版本控制系统(通常是 Git)的远程仓库中。比如使用常见的 git add 命令将修改的文件添加到暂存区,再通过 git commit -m “描述本次提交的内容” 来提交更改,最后使用 git push 将本地分支的提交推送到远程仓库的对应分支上。
2. Lint 执行
触发条件:当代码被推送到远程仓库后,或者在某些配置下可以是在预提交阶段(比如通过 Git Hook 来配置在本地提交前先执行),会触发代码规范检查工具(Lint)的执行。例如在 JavaScript 项目中常用 ESLint,Python 项目可用 Pylint 等。
执行过程:Lint 工具会按照预先配置好的代码规范规则(比如代码格式、变量命名规范、语法最佳实践等)对整个项目代码进行扫描。如果代码不符合规范,它会输出相应的错误和警告信息,告知开发人员哪里需要修改,只有当代码通过 Lint 检查,后续流程才能继续进行。
3. 触发 Hook
Webhook 配置:在项目所在的代码仓库管理平台(如 GitHub、GitLab、Gitee 等)中,会配置对应的 Webhook。Webhook 会监听仓库的某些特定事件(像代码推送这个事件),当这些事件发生时,它会向配置好的 CI 服务器发送一个 HTTP 请求,以此来通知 CI 服务器有新的代码提交,需要开始执行后续的集成流程。
CI 服务器响应:CI 服务器接收到这个来自仓库的 Hook 通知后,会开始准备执行后续的构建、测试等一系列操作,它首先要做的就是根据项目的配置去定位和读取相应的 .yml 文件(比如 .gitlab-ci.yml 用于 GitLab CI/CD,.github/workflows/*.yml 用于 GitHub Actions 等)。
4. 读取 .yml 文件执行命令
解析 .yml 文件:CI 服务器会解析这个配置文件,.yml 文件里定义了整个 CI/CD 流水线的各个阶段以及每个阶段需要执行的具体命令和任务。例如,可能会定义首先执行依赖安装的任务(像在 Node.js 项目中执行 npm install 安装 package.json 里列出的依赖),接着进行单元测试(对于 JavaScript 项目可能使用 jest 等测试框架执行 npm test),再进行集成测试等不同的任务步骤,每个任务步骤都是按顺序依次执行的。
任务执行与环境搭建:在执行每个命令时,CI 服务器会为其创建对应的执行环境,这个环境可能是基于容器技术(如 Docker)搭建的,确保执行环境的一致性,避免因为开发人员本地环境和服务器环境差异导致的问题。如果某个任务执行失败(比如单元测试有部分用例不通过),CI 流程会中断,并将失败信息反馈出来,开发人员需要根据提示去修复代码,然后重新提交触发整个流程。
5. 部署
环境区分:如果前面的步骤都顺利通过,接下来就会进入部署阶段。通常会区分不同的部署环境,比如先部署到测试环境进行更全面的测试,确保功能在接近生产的环境下能正常运行。然后再根据情况部署到预生产环境做最后的验证,最后才是部署到生产环境。
部署操作:部署操作根据项目类型和使用的技术不同而有差异。对于 Web 应用可能是将构建好的项目文件(像前端的静态资源、后端的可执行文件等)通过 SSH 等方式传输到对应的服务器上,并通过脚本(如 shell 脚本)启动相应的服务进程;对于容器化的应用则是将构建好的 Docker 镜像推送到镜像仓库,并在目标服务器上拉取镜像然后启动容器来运行应用。
6. 调用机器人接口发布企业微信群周知
获取发布信息:在部署完成后(无论是部署到哪个环境,都可以选择进行相应的通知),CI 流程中的相关脚本会收集本次部署的关键信息,比如部署的版本号、部署的环境、涉及的功能变更等内容,整理成一条要发布的消息内容。
接口调用与发布:接着通过调用企业微信机器人的接口(按照企业微信机器人提供的 API 文档,使用对应的 HTTP 请求方式,比如 POST 请求,带上消息内容等必要的参数),将整理好的消息发送到指定的企业微信群中,让相关的人员(如开发团队成员、测试人员、运维人员以及其他关注项目进展的人员)能够及时了解到项目的部署情况以及对应的变更信息。

工作大话

你说做了线上问题排查机制,这是?

《MD产品研发流程规范》
目的: 为约束MD事业部产研流程中现存的项目排期混乱、项目分工不明确、上线流程不清晰等问题,MD亟需梳理研发流程,特制定本规范。
需求评审规范:
需求规模: 根据需求计划落地天数(从需求评审至测试完成所经历的工作日),对需求规模进行划分;小需求落地天数1-2天;中等需求落地天数20天内;大需求落地时间大于20天; dp;
需求变更: 若上线前进行需求变更,则评审推迟上线情况;
需求通告: 发布全员可感知的需求通告 ----项目同步群
开发规范:

  1. 责任人
  2. 技术预研
  3. 技术评审
  4. 技术落地
  5. 提测阶段
  6. 代码审核
  7. 研发排期

上线规范:

  1. 上线策略::上线范围(灰度上线/全站上线),上线顺序(前后端上线顺序),数据库字段是否需要刷新等。
  2. 灰度策略:
  3. 数据库修改项
  4. 上线时间:19:00
  5. 上线标准
  6. 表格填写
  7. 线上验收
  8. 回滚策略:
    线上验收底线测试用例未通过,直接回滚;
    客诉过来的白屏,评估是否是新需求导致的白屏,如果为新功能导致的白屏,先回滚后排查;
    测试同事测出来的白屏,根据sentry触发的白屏情况,评估是否回滚;
    客诉 > 2;
    上线后sentry多例报错,开发排查后评估功能异常; 质量事故

线上问题修复
责任人: 根据线上问题原因确定本阶段责任人,开发工程师负责确认问题原因;若原因为开发工程师逻辑缺失或代码缺陷,问题责任人转为开发工程师;若原因为产品经层面辑缺失导致的问题,则问题修复责任人转为对应产品经理;
**线上问题修复阶段的开发流程:**与产品研发流程基本一致;问题责任人负责组织问题参与人,将问题信息(修改方式、变动范围、上线时间等)同步所有参与人,并跟进问题修复细节、问题修复时间及上线时间等重要节点;

线上客诉
**责任人:**测试工程师确认线上客诉后,则将工单流转至开发工程师,开发工程师为客诉负责人;
客诉负责人评估客诉:

  1. 若视为无效缺陷客诉,开发工程师需在工单评论中留言,标明无效客诉的原因,并修改工单状态为无效客诉;
  2. 若视为有效缺陷类客诉,开发工程师需根据客诉类型,确定客诉修改方式、修改范围,跟进客诉修复细节、修复时间及上线时间,并及时处理工单状态;
  3. 若视为疑难客诉,开发工程师将客诉交予疑难问题裁定人进行疑难问题评估,若客诉被评估认定为疑难客诉,则开发工程师需在工单评论中留言,标明疑难客诉的原因,并修改工单状态为疑难客诉;疑难问题裁定人,一般为小组负责人;

《MD 研发质量指标体系》
前端用sentry 后端使用AOM Grafana

线上问题白屏都可能是哪些原因?

当前主要聚焦解决在sentry上报的白屏问题(指通过sentry上报的level:fatal的报错问题,该类问题会引发编辑器崩溃)。目前造成崩溃的问题主要有已下几方面:

  1. 浏览器兼容问题:由于客户浏览器的多样性,导致在开发过程中使用较新的api时,在底版本不兼容,导致底版本不兼容,从而导致页面崩溃。
  2. 业务场景考虑不全,逻辑不完善,逃逸的edge case:例如:动态组件切换为母版组件,选中母版组件下的子组件,ctrl+z将母版组件退回动态组件,子组件依然处于选中态,再修改子组件属性报错等等
  3. 代码逻辑漏洞:例如:组件事件在创建时会先校验再更新到组件,但是事件在粘贴时没有效验直接更新到了组件,造成了脏数据。
  4. 代码边界问题未处理:例如:draft-js组件卸载组件时,dom不存在。
  5. 数据丢失/篡改(根因可能是2,3造成的)

研发流程生命周期

  1. 一个完整项目的研发周期不明确
  2. 研发业务归档不足,且不成体系沉淀。
  3. 编写设计文档
  4. 技术评审:技术评审分为两种,一种是review设计文档,一种是举行技术评审会。两种方式都行,根据项目的复杂度和研发周期决定。默认5天以上的研发需要举行技术评审会,小于5天的只需高职级同学确认技术文档即可。需要在项目同步群中发项目同步消息
  5. 默认研发超过5天的项目,需要在项目期中提交middle code review。超长项目,需要多次提交middle code review。为了避免项目上线前期,提交大量代码进行review。同时也避免在项目实施过程中,研发动作变形产生风险。
  6. 为了提高测试效率,正式提测前,自测不成功的,禁止提测。
  7. 目前final code view都是在测试完成时,进行提测的。但是在解决 final code view 提出的问题,还需要测试进行二次测试,降低效率。
  8. 提测
  9. 上线(灰度、全站),灰度上线非必须,默认原型灰度,非原型全站。研发生命周期的最后一个节点.

支付优化都做了什么?

1.完成了支付仓库优化,因为原先支付在工作台打开过慢,而工作台是我们用户最长停留使用的场景。 所以在工作台使用lib包的形式引入支付仓库,在活动页和定价页使用iframe?to=renew&opener=iframe&param=${param}进行调用。
2.完成了支付链路升级,将价格方案的计算迁移出来 payment.js作为一个单独的npm包,简化支付页面,突出支付金额、可用优惠券、支付方式图标等关键内容;取消提交订单页面改为二维码;承接了订单挽回弹窗,支付方案增加。

为什么ifame会打开慢?而lib包就快了?

  • 资源加载方式
    iframe
    浏览器需要单独请求该iframe所指向的外部资源,这是一个相对独立的完整页面加载过程,会涉及到多次网络请求,包括 HTML 文档、CSS 文件、JavaScript 文件等,而且可能还需要等待这些资源按顺序加载完成后才能进行后续的渲染和交互,因此加载速度相对较慢。
    lib包
    以lib包的形式引入支付仓库时,通常只是加载了一个经过打包和优化的代码库,其包含的是支付功能相关的核心代码,资源体积相对较小且更具针对性。浏览器在加载lib包时,只需要获取这一个文件或少量几个文件
  • 渲染机制
  • iframe中的内容在浏览器中是作为一个独立的子窗口进行渲染的,它有自己的文档对象模型(DOM)和渲染上下文。当浏览器解析到iframe标签时,会在主页面的布局中为iframe预留空间,但需要等待iframe内部的资源加载完成并渲染完毕后,才能将其完整地显示在页面中。
  • 使用lib包时,其代码会直接在当前页面的上下文中执行,与页面的其他部分共享 DOM 和渲染环境。在加载完成后,可以立即与页面的其他元素进行交互和协同工作,无需像iframe那样等待一个独立的子窗口渲染完成,从而在视觉上给用户的感觉是加载更快。

支付优化?

支付成功率 = 成功交易的订单数 / 跳转至支付页面的订单数
支付成功率
1.网络稳定性:前端若频繁出现网络波动,支付请求可能中断。比如在弱 4G 或不稳定 Wi-Fi 环境下,支付流程中的数据传输易出错。
2.优化页面加载速度:减少页面元素数量、压缩图片等资源、采用缓存技术等方式,加快页面加载速度,避免用户因长时间等待而失去耐心,提高支付效率.
3.优化支付链路:去除不必要的步骤和信息填写,同时简化表单布局,使其清晰易懂,让用户能够快速完成支付。
4.系统故障或中断: 解决方案: 设置备用支付网关或冗余机制。
5.用户反馈:收集用户意见,发现流程中的痛点。
6.前端埋点:在支付页面及相关操作流程中设置埋点,记录用户进入支付页面、提交支付请求、支付成功或失败等关键节点的信息,以便后续分析用户行为和支付结果 。

后续你的想法: 1.可视化展示

Promise.all优化?

  1. 划分接口类型 : 明确区分影响首屏渲染的关键接口和非关键接口。例如,展示用户基本信息、首屏商品列表等接口属于关键接口;而加载用户历史订单数量这类在首屏无需立即展示的信息接口,属于非关键接口。
  2. 动态加载 : 事件监听:利用DOMContentLoaded或load事件,在页面加载完成或首屏渲染就绪后,触发非关键接口的请求。

为减少 NPM 包升级带来的成本,可采取以下策略?

1.版本管理策略
2.微前端:

模块化的包管理
每个微前端子应用可以维护自己的依赖包版本,不会强制要求所有子应用同步升级。
示例:某子应用依赖 React 17,而另一个子应用使用 React 16,它们可以独立存在,避免了升级冲突。

渐进式升级
微前端允许逐步升级某些子应用的依赖包,而不会影响整个项目。
示例:可以先在一个子应用中试用新版本的 lodash,观察兼容性后再推广到其他应用。

独立部署
微前端允许独立部署子应用。即使某子应用升级依赖包失败,也不会阻塞其他子应用的功能发布。

灵活的共享依赖
使用模块联邦(如 Webpack Module Federation)共享公共依赖,减少重复下载。

微前端是否适合解决 NPM 包升级问题的判断标准
适合的场景

  • 大型项目,多个团队并行开发,技术栈不统一或需求复杂。
  • 项目长期迭代,依赖包频繁升级且可能影响全局。
  • 需要支持不同模块独立发布、测试和回滚。

不适合的场景

  • 小型项目,团队规模较小,统一技术栈和依赖即可满足需求。
  • 项目中强依赖共享的大型依赖包(如 Webpack、React)版本升级需要全局协调。
  • 对性能要求极高,无法接受子应用带来的额外开销。

营销活动:在负责 618、双 11 前端落地页开发时,遇到过哪些棘手的技术难题,你是如何解决的?

活动大促期间,落地页首当其冲的难题就是高并发场景下页面加载速度过慢。
一方面,对图片、脚本等资源进行压缩使用 TinyPNG、ImageOptim 等专业图片压缩工具,对于一些商品展示图、活动海报等,通过压缩工具处理后,文件大小可能会减小 30%-70%。格式转换:利用工具将图片格式转为 WebP,减小文件大小;
浏览器缓存策略:
通过在服务器端设置 HTTP 缓存头,如 Cache-Control、Expires 等,来控制浏览器对静态资源的缓存时间。对于一些不经常更新的静态资源,如 CSS 样式表、JavaScript 库等,可以设置较长的缓存时间,例如 7 天或 30 天;而对于一些可能会经常更新的资源,如活动配置文件等,则可以设置较短的缓存时间或采用协商缓存。
async和defer: 落地页会引入多个js文件 ,
async:浏览器在解析到带有async属性的

color
border-style
border-radius
text-decoration
box-shadow
outline
background

下面情况会发生重排:

页面初始渲染,这是开销最大的一次重排;
添加/删除可见的DOM元素;
改变元素位置;
改变元素尺寸,比如边距、填充、边框、宽度和高度等;
改变元素内容,比如文字数量,图片大小等;
改变元素字体大小;
改变浏览器窗口尺寸,比如resize事件发生时;
激活CSS伪类(例如::hover);
设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;
查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用getComputedStyl方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”;
.
缓存 DOM 查询结果:如果在代码中多次需要获取同一个 DOM 元素,将查询结果缓存起来,避免多次重复查询导致的性能开销。减少重排
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。

前端SEO搜索引擎优化的技巧?

它是一种透过了解搜索引擎的运作规则来调整网站,以及提高目的网站在有关搜索引擎内排名的方式
网页优化3剑客,title、description、keywords;
1.创建唯一且准确的网页标题。
2.使用 meta description
我们可以使用 meta description 标签来准确概括总结网页内容。我们应避免内容中出现关键词的堆砌、描述过长、描述过于笼统简单,如直接拷贝关键词或正文内容、或“这是一个网页”这种没有实际性意义的描述等现象。
3.使用 meta keywords
我们可以使用 meta keywords 来提取网页重要关键字,如:

4. 网页优化的小技巧网址logo:
并不是img标签插入,而是超链接a标签背景
5. 使用语义化元素:
例如使用 h1可以让爬虫知道这是很重要的内容。然而,值得注意的是,例如在想要表达强调时,我们不应该滥用标题元素或者 b 、 i 这种没有实际意义的标签,换而可以使用 em或 strong来表示强调。此外, h1的权重比h2的大,我们不应该为了增大权重而去滥用 h1,一般来说 h1用于正文的标题。
6.利用 中的 alt 属性 : alt 属性可以在图片未成功显示时候,使用文本来代替图片的呈现,使“爬虫”可以抓取到这个信息。此外它还可以解决浏览器禁用图像或屏幕阅读器解析等问题。

7.提高加载速度
我们应尽量让结构(HTML)、表现(CSS)及行为(JavaScript)三者分离。如果在一个 HTML 页面中,编写大量的 CSS 样式或脚本,会拖慢其加载速度,此外,如果不为 定义宽高,那么会引起页面重新渲染,同样也会影响加载速度。一旦加载超时,“爬虫”就会放弃爬取。

linux命令知道哪些?

常用linux

docker的理解?

我们造一间房子,需要设计图纸、搅拌水泥、搬砖头、砌墙…,然后才能把房子造好
下次换个地方造房子,上面的步骤需要再来一遍,非常麻烦。
假设我们有一种神奇的方式,可以把之前造的房子拷贝一份,然后把这个拷贝放到背包里,
带到新的需要造房子的地方,用这个拷贝复制出一间一模一样的新房子。这样就可以快速造出一模一样、分毫不差的房子了
这种神奇的方式就是Docker容器技术。房子的拷贝就是镜像(Image),放拷贝的背包就是仓库(Repository),用拷贝造出的新房子就是容器(Container)。
优势
轻量级与快速部署 :容器的启动速度非常快,通常在秒级甚至毫秒级,相比传统的虚拟机启动时间大大缩短。这使得在开发、测试和部署过程中能够快速迭代和部署应用程序。
一致性环境 :无论在开发、测试还是生产环境,Docker 容器都可以保证应用程序运行在完全相同的环境中,避免了因环境差异导致的问题,提高了应用程序的可移植性和稳定性。
资源高效利用 :由于容器的轻量级特性,多个容器可以在同一台宿主机上运行,并且可以根据应用程序的需求灵活分配资源,提高了硬件资源的利用率。
易于管理和扩展 :Docker 提供了丰富的命令行工具和 API,方便用户对容器进行管理和操作,如创建、启动、停止、删除容器等。同时,基于 Docker 的容器编排工具如 Kubernetes 等,可以方便地对大规模的容器集群进行管理和扩展。在Linux中使用Docker管理容器可以按照以下步骤进行:

  1. 首先,需安装Docker。可以在Linux系统上打开终端,并运行以下命令来安装Docker:
sudo apt-get update
sudo apt-get install docker-ce
  1. 安装完成后,启动Docker服务:
sudo systemctl start docker
  1. 检查Docker服务是否正在运行:
sudo systemctl status docker
  1. 使用Docker命令行工具来管理容器。以下是一些常用的Docker命令:
  • 拉取镜像:docker pull <image_name>
  • 运行容器:docker run <options> <image_name>
  • 查看正在运行的容器:docker ps
  • 查看所有容器(包括停止的):docker ps -a
  • 停止容器:docker stop <container_id>
  • 删除容器:docker rm <container_id>
  • 查看镜像:docker images
  • 删除镜像:docker rmi <image_id>

以上只是一些基本的Docker命令,你还可以使用更多的Docker命令进行容器的管理。详细的Docker命令和用法可以参考Docker官方文档或使用docker --help来获取帮助信息。

如何搭建前端监控平台?

埋点是将用户行为数据化的最常用方式;由于大规模数据读写成本非常高,所以会根据埋点的优先级和消费场景,将埋点进行实时和离线不同的上报链路。
必要时会使用降级等方式应对高并发场景。

非常全的从0到1
一个完整的前端监控平台包括三个部分:数据采集与上报、数据分析和存储、数据展示
在这里插入图片描述
1.用户行为数据采集
用户行为包括:页面路由变化、鼠标点击、资源加载、接口调用、代码报错等行为
2.个性化指标
执行时间超过 50ms 的任务,被称为 long task 长任务
3.首屏加载时间
首屏指的是屏幕内的 dom 渲染完成的时间,计算首屏加载时间流程:

1)利用MutationObserver监听document对象,每当 dom 变化时触发该事件

2)判断监听的 dom 是否在首屏内,如果在首屏内,将该 dom 放到指定的数组中,记录下当前 dom 变化的时间点

3)在 MutationObserver 的 callback 函数中,通过防抖函数,监听document.readyState状态的变化

4)当document.readyState === ‘complete’,停止定时器和 取消对 document 的监听

5)遍历存放 dom 的数组,找出最后变化节点的时间,用该时间点减去performance.timing.navigationStart 得出首屏的加载时间
在这里插入图片描述
错误数据采集
错误信息是最基础也是最重要的数据,错误信息主要分为下面几类:

  • JS 代码运行错误、语法错误等 异步错误等 静态资源加载错误 接口请求报错
    公共信息的采集,可以让我们从更多维度对数据进行串联和分析
    用于更精细场景的用户数据分析

埋点上报:

埋点从客户端通过多种 HTTP 请求方式上报到服务端
原生 APP、服务端的埋点上报,多采用 protocol buffer 协议
图片上报 xhr post sendBeacon
在 .gif 图的 url 上拼接埋点信息常规的进行上报 xhr post 请求上报点信息在 body 里,埋浏览器推荐的埋点上报方式,也是 post 请求

浏览器兼容性好,请求方式简单 ; 各平台接口一致,可以进行更复杂数据处理如加密、压缩; 上报更稳定;性能影响更小;

影响页面跳转性能;不适合非 web端场景不适合复杂数据处理有要求; 页面关闭易丢失;对 content-type 仅限浏览器场景;无 respond信息,否则出现预请求只返回 true/false

图片打点上报的优势:
1)支持跨域,一般而言,上报域名都不是当前域名,上报的接口请求会构成跨域
2)体积小且不需要插入 dom 中
3)不需要等待服务器返回数据

图片打点缺点是:url 受浏览器长度限制

数据上报
优先使用 requestIdleCallback,利用浏览器空闲时间上报,其次使用微任务上报

埋点分析

当前埋点主要使用商业化产品【神策】和开源工具 【Sentry】,都是比较主流的埋点工具。
但中间过程性的埋点统计相对较少,在统计到相关结果指标后,想对结果进行归因分析较为困难。

埋点的思考:
在这里插入图片描述

组件库编写规范:

组件库编写规范涵盖了多个方面,以下是详细的规范要求:
代码风格与规范
遵循统一规范:采用统一的代码风格指南,如 ESLint 配置规则,确保代码风格一致,提高代码可读性和可维护性。
代码结构清晰:组件代码应具有清晰的结构,包括组件的定义、属性、方法、生命周期钩子等部分,各部分之间逻辑清晰,易于理解。
组件设计原则
单一职责:每个组件应只负责一项特定的功能,功能单一使得组件易于理解、维护和复用。
可复用性:组件应具有高度的可复用性,设计时要考虑到不同场景下的使用需求,提取公共部分,避免硬编码。
可扩展性:组件应具有良好的可扩展性,能够方便地添加新功能或修改现有功能,通过合理的接口设计和插件机制等方式实现。
组件 API 设计
属性设计:组件的属性应具有明确的含义和用途,遵循驼峰命名法,属性类型应明确,支持多种数据类型,并提供合理的默认值。
事件设计:组件应定义清晰的事件,用于与外部进行交互,事件名称应具有描述性,遵循小驼峰命名法,同时提供必要的事件参数。
方法设计:组件暴露的方法应具有明确的功能和用途,方法名应具有描述性,遵循小驼峰命名法,方法的参数和返回值应明确。
组件开发流程
需求分析:在编写组件之前,需要对组件的功能需求进行详细分析,明确组件的使用场景、功能要求、与其他组件的交互等。
设计阶段:根据需求分析的结果,进行组件的设计,包括组件的接口设计、内部结构设计、样式设计等。
开发与测试:按照设计方案进行组件的开发,开发过程中要遵循组件编写规范,开发完成后进行严格的单元测试和集成测试,确保组件的质量。
文档编写
组件说明文档:为每个组件编写详细的说明文档,包括组件的功能描述、使用方法、属性说明、事件说明、方法说明等,帮助用户快速了解和使用组件。
API 文档:提供组件的 API 文档,以清晰的格式列出组件的属性、事件、方法等接口信息,方便开发人员查阅。
示例代码:为组件提供丰富的示例代码,展示组件在不同场景下的使用方法和效果,帮助用户更好地理解组件的使用。
样式规范
样式命名规范:采用统一的样式命名规范,如 BEM(Block Element Modifier)命名法,确保样式名具有明确的含义和层次结构,避免样式冲突。
样式结构清晰:组件的样式应具有清晰的结构,按照组件的功能模块或结构层次进行组织,便于维护和修改。
样式兼容性:考虑到不同浏览器的兼容性,组件的样式应进行兼容性处理,确保在主流浏览器中显示效果一致。
版本管理
语义化版本号:组件库应采用语义化的版本号,遵循 MAJOR.MINOR.PATCH 的格式,明确版本的升级规则,方便用户了解版本的变化情况。
版本发布流程:建立规范的版本发布流程,包括代码合并、测试、发布等环节,确保每次发布的版本质量稳定可靠。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值