腾讯秋招一面
自我介绍。
📌 回答
面试官您好,我叫 xx,本次面试的意向是前端开发工程师。在兼顾学校课程的情况下,我发现自己对于用户交互等工作很感兴趣,因此自学前端,到目前为止零零碎碎加起来学习一年出头。
先简单介绍一下技术方面的经验,除了语言基础外,自己主要接触过 Vue 和 React 框架及其生态库,目前主要学习方向在 React。学习过程中会自己根据开源,做一些项目,也体现在了本次简历上。最早的时候做过一个 IT 知识前后台,前台采用 CRA,后台采用 Umi 构建了发帖、评论、数据管理等常见功能。之前用 Vue 快速迭代过一个低代码平台,目前正在用 React 做重构,力求实现一个低代码平台的真实场景复现。
离我最近的一段前端经历是在字节跳动做实习工作,当时团队提出了一种解决方案,能实现多个中后台的统一管理。其中,我主要负责了两个平台相关的工作。一个是算法业务相关的平台,涉及到的工作主要是对算法业务的支持,包括特征淘汰、特征调研等界面支持以及数据看板搭建;另一个是元数据平台,在这个平台上主导引入 AI,助力新同学由自然语言构造我们内部的 DSL 数据查询语言。实习期间的技术栈包括 Garfish 微前端、BFF、MonoRepo、React、TypeScript。
目前为止,我学习的重心主要在 React,同时平时会自己使用 Express、Nest 等开发后端程序和中间件服务,对常见的 MongoDB、MySQL 等数据库服务也有所了解。后续的学习重心可能会向全栈、AI 赋能等方向发展。
从运行、开发、部署等方面介绍低代码平台的流程。
参考 ZRead 技术文档
📌 回答
低代码平台目前还未重构迁移完成,是 Vue 技术栈搭建的。平台主要由两三大部分组成——物料市场和页面编辑,以及管理所有问卷的仪表板中心。整体功能上来看,页面具有生成、保存、预览、导出为 PDF、在线访问的功能。
从开发方面来讲,核心技术采用了 Vue3、ElementPlus、Pinia、VueRouter、TypeScript、Dexie、Vite。封装组件层面上来看,主要划分为按钮等通用组件、编辑器画布渲染组件、素材市场内部展示组件、配置面板组件四部分;数据流层面上来看,主要经历用户交互、Pinia 状态更新、Dexie 数据持久化、UI 重新渲染四步循环。另外,尽管目的是尽量不依托服务器,但对于在线访问仍然设置了 Express 进行 HTTP 通信。
部署方面,我平时会用到 Docker、Github Pages、Vercel 等手动的 CI/CD 策略,但出于快速迭代目的,Vue 端代码上传到 Github 仓库之后直接进行了第三方授权,由国产帽子云来自动部署。
🔍 仓库各自存储了什么?
实现了物料市场和编辑器页面的双模架构,因此会有两个 Pinia 仓库。
MaterialStore 里存了 components 和 currentMaterialComponent,前者用来保存所有物料信息,后者控制当前单独展示的物料渲染,以及选择物料区域里选中的高亮效果。
EditorStore 里存了 currentFoucusedComponentIndex、questionCount、components,分别用来控制当前问卷选中的组件、问卷共有几个问题、问卷渲染了哪些组件。
数据不止存在 Pinia,为了实现数据持久化,用 IndexedDB 来取代服务器,并且用 Dexie 库来简化原生 IndexedDB API 书写。定义了一个数据库,里面有问卷表,字段包括 自增主键 id、createdAt、updatedAt、title、questionsCount、components。
🔍 你的 Schema 大致是如何定义的?
首先,每个组件都会有一个 Schema 对象,包含组件类型 type、名称 name、随机 ID,其中 type 是通过 markRaw 来映射组件的渲染效果的,而 name 只是简单的一个名称,另外还有组件的配置状态,可以将其称之为 status。在 status 里有很多字段,而这些字段对应的 value 对象包含 id、status 来标识当前值、isShow 来控制当前组件是否渲染这一字段配置功能、并提供 editComponent 字段可以在页面选中组件之后通过注册中心 Map 控制配置区域对应渲染输入框还是其他形式,另外还提供 name 字段来解决本地存储之后 editComponent 丢失现象,主要依赖注册中心二次恢复机制。
如果想要新增组件,你如何实现发包并注册到自己的物料市场,以供用户的选择?
📌 回答
当前如果想要新增组件,用户需要在项目里手动执行以下三个步骤:① 去创建想要渲染出来的 UI 组件 ② 在配置字段里和默认值文件里添加相关逻辑 ③ 在映射文件里建立新的映射。
而实际上,低代码平台理想情况下应该支持第三方组件的添加。我提供以下几种方案吧:
- 维护一个专门的目录 extensions,第三方组件所有配置信息都放在其中的一个文件里,交由项目来扫描并生成之前需要手动维护的文件内容。缺点也很明显,依然依赖于项目自身,开发者还是需要走项目代码改动这一条路。
- 远程动态加载:用户将第三方组件打包成 UMD 或者 ESM 并上传到 CDN,平台提供导入组件功能,输入 URL 之后用 import 等手段动态加载模块。优点在于用户不用改动平台代码,缺点在于对于安全性和兼容性难以控制。
- 第三方将他们的组件打成 NPM 包,我们来实现导入这些包。和上一条区别在于上一条用户上传 JS 等文件,这里是一个正规的 NPM 包。但都要依赖注册机制。
- 再往上走,可以做一个插件市场。平台 UI 仍然提供注册入口,用户上传包括组件 UI 和 meta.json 之后,平台存储到后端数据库或者 CDN,前端动态加载。
在发 NPM 的时候会提前构建吗?
📌 回答
是的,需要提前在本地做构建。整体上来看,组件库的发布其实会有几个步骤:
- 在本地编写源码;
- 配置 Vite、Webpack 等构建工具;
- 执行构建命令,期间可能涉及到代码的压缩、移除等;
- 拿到 dist 目录之后需要调整 package.json,指定入口文件;
- 执行 publish 命令完成发包。
用户经过拖拽等操作发布页面,你如何展示出来页面?
📌 回答
目前的话,前端点击生成页面会得到一个 url,附带着当前问卷的 ID。会有一个简易的服务器,存储 ID 和当前页面用到的组件的列表。前后端通过 HTTP 通信,得到 ID 对应的组件列表之后做页面渲染。
重构之后打算用服务端渲染,整体思路是这样的:服务端依然需要一个表来存储页面上一些元信息,前端请求还是带上 url 里的 ID,服务端从数据库里读取页面相关配置,生成 HTML 字符串。前端拿到这样的静态 DOM 之后,进行必要的水合,实现页面动态化。
用户生成的页面会有许多物料,包括你提及的 SSR,如何去做性能的优化?
📌 回答
- 客户端方面,可以实现组件按需渲染,SSR 只负责渲染首屏,剩余非首屏内容交由客户端来进行滚动监听等判断渲染;可以尝试渐进式组件激活避免全量注水;可以批量操作 DOM 减少重排重绘。
- 服务端方面,可以考虑 LRU 缓存,对于常用的一些物料或页面等避免多次计算。
- 网络方面,可以设置 Gzip 等压缩策略,同时可以控制一下配置项,对于冗余字段可以做计算裁切。
微前端底层的原理。
📌 回答
- 微前端主要需要负责三个工作,路由劫持、应用渲染和沙箱隔离。
- Garfish 会改写路由相关的 API,通过子应用接入时候的配置,来确定不同路由激活什么应用。另外,对于路由冲突问题,Garfish 还会计算出子应用的唯一 baseName 来解决。
- Garfish 的 Loader 要做的工作很多,包括资源的下载和解析、AST 构建、环境初始化、编译、容器创建、DOM 渲染、生命周期的维护等。
- Garfish 沙箱环境分为快照沙箱和 VM 沙箱。CSS 方面,Garfish3 类似于快照,子应用激活和卸载时,完成 DOM 记录和恢复;Garfish5 采用了收集模式,对于静态的 CSS 文件,会将其挂载在子应用的渲染容器上,实际上是类似于 scoped 的思想。对于动态添加的样式内部会做访问转发,通过 Proxy 将对 head 添加样式的请求转移到了容器节点上。但是由于没有做 namespace,对于多实例的支持欠佳。
- JS 方面,Garfish 之前采用快照模式,基于 eventListener 来做事件的注册和卸载,实现比较简单,但是子应用和主应用可能有事件冲突,并且不支持多实例;目前采用的是 VM 沙箱,基于兼容性更好的 ProxyPolyfill 模拟一个伪造出来的 window,并按照规定的 UMD 格式导出代码并挂载,就能做到对变量的访问被转发到某个层级。借此,子应用卸载之后,这些变量也会随之卸载。还有一些细节上的手段,对于一些 setTimeout 之类的 API,Garfish 自己做了重写;对于 webStorage 的访问,也会做劫持,添加上 key 来标识前缀,完成数据之间的隔离。
- 阿里系的 qiankun 微前端和字节 Garfish 实现策略基本一致。
- 腾讯 Wujie 微前端差异比较大,CSS 方面是依赖 Shadow DOM 实现的,JS 方面思路如下:基于 Iframe 做到全局变量的隔离,子应用 DOM 渲染在一个 Shadow DOM 中,弥补 Iframe 尺寸限制、样式个例难的问题。至于 Iframe 带来的通信问题,框架自身有代理转发机制、全局事件总线、上下文绑定等手段。
提到了 CSS 和 JS 的隔离,DOM 树是否需要隔离?
📌 回答
Garfish 在这方面的实现手段依然是三部分:
- 子应用容器创建时,容器的 id 或者类名加上 Garfish 生成的 HashId 保证唯一性;
- 子应用挂载到这个唯一容器里,而不是像传统 CSR 框架一样在 app 节点;
- 对关键性 API 劫持改写,防止访问到全局的 document 或者 window
技术方面是否采用 MonoRepo 会有什么差异或者说优缺点?
📌 回答
MonoRepo 相比 MultiRepo,最直接感受到的一点就是所有项目的代码都放在一个代码仓库里,这也是它的定义。
优点在于:
- 集中管理代码可以实现高效协作:团队成员不需要跨项目修改代码,而且单个仓库也保证了分治策略的同意,适合团队开发。
- 依赖管理简化:内部模块的引用可以通过相对路径或者 workspace,避免了发布到 NPM 的繁琐流程,也规避了单独发布模块开发人员需要修改代码中版本的过程。
- 所有代码都可以共享工程化环境:Prittier、ESLint、Babel 等配置统一,保证了项目的代码风格、构建规则等一致性。而且,MonoRepo 便于统一接入 CI/CD 流程,一次配置可以覆盖所有自动化测试、构建和部署。
- 原子化提交:某次跨项目的修改可以通过单次的提交来完成,提交历史更连贯。
缺点在于:
- 仓库体积膨胀导致性能下降:项目增多后 node_modules、构建产物等容易引起开发体验下降,如果这些产物被放在远端仓库,也会导致克隆仓库等 Git 操作缓慢。
- 权限管理复杂:MonoRepo 不支持仅允许某个团队操作某个特定项目的精细化控制,需要依赖其他手段来实现。
- 学习成本高:需要选择 workspace、lerna 等合适工具,团队成员学习成本会变高。并且,在配置不当情况下,可能会造成模块耦合过度、构建产物污染的问题。
- 依赖混乱风险:多个项目对于依赖的过度共享可能会引起依赖混乱,开发人员需要分出一定的心智来承担这部分的保障工作。
async 和 defer 的作用与区别。
📌 回答
默认情况下,浏览器在解析 HTML 过程中遇到 JS 会被阻塞。async 和 defer 是缓解 JS 阻塞、提升渲染效率的一种手段。
- async 标注的 JS 会异步加载,并在下载完成后立即执行。其异步性体现在脚本下载过程中,仍然支持 HTML 的继续解析。但是执行的时机无法明确,多个 async 脚本执行原则是谁先下载完谁先执行。这适合一些独立脚本,如网站统计分析服务等。
- defer 标注的 JS 脚本主打一个延迟执行,等待 HTML 解析完成后顺序执行。脚本下载过程中,和 async 一样支持 HTML 的继续解析。但区别在于,代码书写的顺序是怎么样的,执行的顺序就是怎么样的;而且执行时机不是下载完立马执行,而是在 HTML 解析完成之后、DomContentLoaded 事件之前,延迟执行。因此,适合一些保证执行顺序的脚本;同时,适用于存在依赖关系的脚本,比如 jQuery 依赖它的业务脚本。
如何隐藏页面上的一个元素?
📌 回答
- display 设置为 none 实现完全隐藏;
- visibility 设置为 hidden 从视觉上隐藏但仍然占有原来的布局空间,比如临时隐藏的步骤指示条;
- opacity 设置为 0,和 visibility 区别在于还可以交互,比如渐隐渐显动画等;
- 绝对定位将元素偏移到屏幕之外,但仍存在,可以通过 JS 操作 DOM,适合一些临时隐藏但后续可能恢复的情况;
- transform 设置缩放到 0,适合收缩动画等;
- clip-path 裁剪路径,设置为 inset(100%)。
如何适配暗黑模式?
📌 回答
- 设置两套变量,结合媒体查询 prefer-color-scheme,代码层面用对应变量编写。在用户切换的主题的时候,界面会采用不同变量,实现主题的切换。
- 也可以采用暴力一点的手段,对于 light 和 dark 类名的 DOM 分别采用对应的 CSS 样式段落,并且可以在 webStorage 存储用户的偏好,。
- 一些第三方组件库会自带暗黑模式,可以根据文档配置。
- 目前 CSS 提供一些新的 API,比如 light-dark()函数可以一行代码配置不同变量,减少代码书写量;filter 设置 invert 和 hue-rotateb
Sass/Less 等预处理器对于手写 CSS 的优势在哪里?
Tailwind 相比于预处理器,解决了什么问题?
Tailwind 怎么实现有略微差异的相似样式管理?
如何去除一个字符串首尾的空格?
📌 回答
- trim 可以直在 React 中,Fragment 是一个特殊的组件,用于在不添加额外的 DOM 元素的情况下,将多个子元素组合在一起。它解决了 React 组件中一个组件必须返回单个根元素的限制。
在代码书写层面,可以导入 Fragment,用它来包同级组件,也可以直接使用空标签的短语法。
要注意的是,Fragment 还支持传入 key 属性,但也只支持传入 key。接去除首尾的所有 whitespace 字符。
- trimStart 和 trimEnd 分别去掉头部和尾部的空格。
- 采用指针遍历,非空字符加到结果字符串做拼接或结果数组做 join。
- replace 方法匹配空格等字符,也可以使用正则表达式。
import 和 require 的区别。
什么情况下 JS 代码会引起内存泄漏?内存是如何回收的?
📌 回答
常见的内存泄漏场景:
- 意外的全局变量,比如未声明直接赋值的变量会被挂载到 window 或者 global 上,成为全局变量,不会被自动回收。
- 闭包导致的内存滞留,闭包会持有外部函数的变量引用,未被释放的闭包里变量引用会持续存在。
- 操作 DOM 的情况下,DOM 被移除但是 JS 代码中仍然引用,浏览器无法回收该元素。
- 定时器或者事件监听器没有清除。
- 缓存没有限制大小:Map 等缓存机制如果持续增长,会导致内存占用过高。
JS 内存回收机制:
- 标记清除:从根对象触发,给所有可达对象做标记,回收阶段对于不可达对象做统一回收。现代浏览器对此也有优化手段,主要分为分代回收和增量标记。分代回收将对象分为了新生代和老生代,不同代采用不同的策略;增量标记是为了避免长时间阻塞主线程。
- 引用计数:跟踪每个对象被引用的次数,当引用次数为 0 时回收对象。缺陷在于无法解决循环引用,当两个不可达对象互相引用的时候,浏览器无法回收。因此,现代 JS 引擎很少使用这种回收机制。
如何做到内存泄露的排查?
📌 回答
- 浏览器环境:① 观察 Memory 面板,利用快照模式辅助定位内存的泄漏源。② 观察 Performance 面板,检查内存曲线是否持续上升或稳定在高位。③ Coverage 检测未使用代码。④ 在控制台输出 performace.memory。
- Node 环境:① 运行命令中添加 --inspect 参数,打开 chrome://inspect 连接调试,使用与浏览器相同的 Memory 面板分析堆快照。②使用 clinic 等专用诊断工具,可以生成内存泄漏报告。③ process.memoryUsage() 监控内存使用情况。
通过 Git 提交了代码之后如何做撤销?
rebase 和 merge 的区别和各自的优缺点。
用户反馈首屏较慢如何做优化?
📌 回答
- 减少压缩体积:① 使用代码压缩和 Tree-Shaking。② 图片等可以使用 WebP、AVIF 替代 JPEG、PNG 等,实现格式优化。③ 首屏只加载可视区域图片,其他用 loading="lazy" 延迟加载。④ 通过工具压缩图片,或者 CDN 裁剪服务。⑤ 简单图片可以使用 SVG 代替或者精灵图合并,减少请求次数。⑥ 第三方库寻找更轻量的来代替。
- 优化加载策略:① 路由懒加载。② 关键 CSS 用内联,而不是外部文件。③ 异步加载 JS 等非关键资源。④ prefetch、preload、preconnect。
- 网络传输优化:① 可以采用 HTTP。② CDN 加速静态资源。③ Gzip 或 Brotli 压缩文本资源。④ 设置缓存策略。
- 渲染与代码优化:① 减少 DOM 节点数量,避免渲染开销。② 避免渲染阻塞,比如 requestAnimationFrame。③ SSR 或 SSG。
讲一下什么是 HTTPS,为什么理论上会比 HTTP 更安全?
Vue 的 nextTick 是如何来实现的?
useState 和 useReducer 两者之间有什么区别?
useEffect 和 useLayoutEffect 的区别?为什么需要 useLayoutEffect?内部是怎么实现对 DOM 的更精准的控制,可以考虑时机等方面。
讲一讲 React 中的 Fragment。
📌 回答
在 React 中,Fragment 是一个特殊的组件,用于在不添加额外的 DOM 元素的情况下,将多个子元素组合在一起。它解决了 React 组件中一个组件必须返回单个根元素的限制。
在代码书写层面,可以导入 Fragment,用它来包同级组件。也可以直接使用空标签的短语法,在解决 DOM 冗余的情况下还能保持代码的干净整洁。
要注意的是,Fragment 还支持传入 key 属性,但也只支持传入 key。
React SSR 的原理?
React 和 Vue 都有各自的数据管理库,其存在的意义是什么?只是解决通信问题吗,那为什么不把变量都定义在全局来做到跨组件的数据控制?
📌 回答
- 数据管理库的存在,可以很好地解决跨层级组件之间的通信问题。对于父子组件,可以通过 props 来做通信;对于结构简单的组件之间,也可以用 useContext 等局部上下文通信;如果只是涉及到了事件通信,可以考虑 EventBus。但是随着组件嵌套层级变深,关系维护不便,数据管理库在这个时候就可以发挥作用了。
- 如果在全局保存数据变量,可能会有多个组件都依赖这个变量,状态变更的不可追溯和副作用的不可预测,随着业务规模扩大都会带来灾难。数据管理库也可以解决这个问题,比如 Redux Logger 可以记录每一次变更,并且采用数据管理库也让每次修改都有了明确的 action,同时支持时间旅行调试等。Redux 这样的单向数据流还可以减少不可预测性。
- 不同组件依赖不同变量,但如果都在全局维护明显耦合性很高。库的存在能让你做到,不同模块可以应用不同的 store,做到内聚。
- 数据还可能和一些别的逻辑,如异步请求等相关。webStorage 等存储方案并没有这样的功能,但是我们可以在 store 里书写这样的代码。
- 数据管理库和特定框架之间的配合,可以减少不必要的渲染。比如,Redux 配合 useSelector 可以实现精确订阅,只在依赖变化的时候重新渲染。
- 还有一些复杂场景,比如可以通过中间件拦截未授权的 action,结合 persist 等库实现数据持久化,通过 middleware 区分不同环境的状态处理逻辑。
列表渲染的 Item 为什么不建议用 index 去做 key 值?
📌 回答
React、Vue 等框架地列表渲染中,不建议使用数组 index 作为 key 地主要原因是 index 作为 key 可能会引起组件状态异常或者导致性能问题。
- 当列表发生增删、排序等改变顺序的操作时,index 会跟着变化,导致框架误判组件的身份,进而引发状态错误。比如数组渲染出来的两个 input,此时如果 unshift 第三个输入框,React 误判之后会把原来第一个 input 的内容错位到新的 input 上。
- 当用 index 作为 key 的时候,列表顺序变化会导致大量的 key 变化,框架会以为所有组件都需要重新创建,浪费性能。
能用 index 作为 key 的有以下几种情况:
- 静态列表,不存在增删、排序等情况。
- 列表项没有自己的状态,如输入框内容。
- 列表项不会被复用到其他地方。
但是即使满足这些情况,也不推荐用 index 作为 key 的习惯,因为需求可能变化。
有没有用 Node 写过服务或者工具?和数据库之间的交互采用什么框架?交互的流程是什么样的?Nest 和 Express 相比好处在哪里?数据操控方面有没有进行过别的选型调研,为什么最后选用 Prisma?
有没有做过小程序?你觉得像 Uniapp 这样基于底层封装的框架,会有什么好处和坏处?
有了解过 Tree-Shaking 是什么?它的原理是什么?
📌 回答
Tree-Shaking 是前端构建工具中用于消除未使用代码的优化技术,目的是减小最终打包文件的体积。
Tree-Shaking 能实现的核心依赖两个条件:
- ESM 静态特性:ESM 的导入导出语句是静态的,在编译时就能确定模块之间的依赖关系,构建工具打包的时候就能清晰地分析出哪些代码被引用、哪些代码没有被引用。与之相对,CJS 模块是动态的,只能等到运行时才能确定彼此之间的关系,因此不支持树摇。
- 代码副作用分析:构建工具需要判断代码是否存在副作用,简单来讲就是代码的执行是否会对外部的环境产生影响,比如修改全局变量、操作 DOM 节点等。对于这类代码,即使没有被引用,也可能不能删除。开发者可以通过配置 package.json 中的 sideEffects 字段,提示工具哪些文件无副作用,帮助构建工具更精准地进行 Tree-Shaking。
如何封装实现一个通用模态框?
你之前的工作或者实习中,讲一下你主动发起对工作流程中痛点的优化。
相比于其他校招候选人,你觉得自己的优点和缺点是什么呢?
「反问」 团队的技术栈?
「反问」 团队对于 AI 是否有一些业务或开发的探索?
「反问」 面试官所在部门的团队业务范围?
「反问」 是否会采用微前端等技术架构?