07-面试讲解
# 公共表单模块面试讲解
# 技术点图谱
涉及到的技术点如下:
- 类的设计
- localstorage
- sessionstorage
- BroadcastChannel
- 其他标签页通信方案
- 测试与压缩
- 包的发布
# 难点描述
模拟问题:我看到你写的项目亮点是“实现表单数据的同步与状态保持”,你能简单说一下这具体是做了什么事情么
问题分析
- 先介绍项目背景
- 描述清楚遇到的问题
- 你拿到这个问题是如何思考的
- 你思考出来的落地方案
- 落地方案的效果,最好有具像化的数据
- 强调封装为了公共模块,并发布至npm
参考答案
当时我们的项目是一个比较复杂的后台管理系统,里面涉及到很多的表单需要用户填写。由于表单内部涉及到的表单项比较多,往往需要用户填写的时间会比较长,再加上还有多标签页、多窗口之间的操作,所以当时遇到了几个不得不解决的问题:
- 数据丢失风险:就是用户在填写表单过程中,可能会因为浏览器意外关闭、系统崩溃等原因导致填写的数据丢失
- 表单数据恢复:用户在未提交表单数据的情况下,重新打开页面时需要恢复之前填写的内容
- 跨标签页数据同步:当用户在多个标签页同时编辑一个表单时,需要确保不同标签页之间的数据同步,避免数据不一致
- 去除本地冗余数据:表单提交后,需要及时清理本地的数据,防止存储数据冗余。
(阐述项目背景以及遇到的问题)
当时拿到这些需求后,我先对功能需求进行了归类和拆解,最终整理出来其实就是两大块:
- 数据自动保存与恢复
- 多标签页间数据广播
这两个功能要实现起来其实并不难,而且实现的方案还不止一个。但有一个非常关键的点,就是我考虑将这个功能模块封装为一个不受框架限制的公共模块,无论是在 Vue 还是在 React 里面可以使用。(你是怎么思考的)
因此最终在对这个模块进行设计的时候,我将其设计为了类的结构,并且内部代码不会使用某个框架特定 API. 这样才能确保这个模块无论是在 Vue 中还是 React 中都是适用的,哪怕是放到 Svelte 或者 Solid.js 里面,我封装的这个模块也能正常工作。(落地方案)
有了这个模块做支撑后,前面我所说的那个问题都得到了有效的解决,并且为了方便团队其他成员使用,我先将其发布到了公司内部的包管理服务器上面。后面由于该模块在公司多个项目里面都工作良好,最后还将其发布到了 npm 上面。
# 技术点讲解
# 1. 类的设计
模拟问题:既然是封装成公共模块,那么要考虑的地方肯定相比业务代码就要多一些,你能讲一下你当时在封装这个模块时,有哪些考虑么?
问题分析
贴合你的项目,说一下你当时在设计类的时候,是怎么考虑的?每一个类它的职责是什么?
因为是类的设计,所以最好说一下用到了什么样的设计模式
参考答案
当时我在设计模块的时候,整理出来了两个功能模块。一个 FormStorage 类专门负责处理:
- 数据本地存储、恢复、删除
- 多标签的数据广播
而另一个 FormStorageManager 类则是专门管理 FormStorage 类实例。
之所以要用 FormStorageManager 来管理 FormStorage 类实例是为了:
- 集中管理:通过 FormStorageManager 类来集中管理所有 FormStorage 实例,确保所有实例的统一初始化、数据保存和事件处理。
- 优化事件监听:避免在每个 FormStorage 实例中重复绑定全局事件监听器,从而减少不必要的性能开销。
另外,FormStorage 类里面用到了 BroadcastChannel 来实现跨标签页的通信,这也算是观察者模式的一种体现。(说到了设计模式,体现对设计模式也是有一定了解的)
- 广播方:FormStorage 实例通过 BroadcastChannel 发送和接收消息,实现数据同步。
- 订阅方:多个 FormStorage 实例监听 BroadcastChannel 消息,接收并处理数据变化通知。
# 2. 本地存储
模拟问题:说一下本地存储这一块儿有哪些方案?你为什么选择了 localstorage ?
问题分析
- 阐述常见的本地存储的方案有哪些?各自的区别是什么?
- 为什么选择了 localstorage?
- 设置下一个钩子
参考答案
本地存储这一块儿,其实客户端的解决方案还是挺多的,常见的有:
- localstorage:同步存储,每个域名下有 5-10MB 的存储空间,适合存储少量的字符串数据,数据持久性高,即使浏览器关闭,数据仍然存在
- sessionstorage:与 localstorage 类似,同样只能存储字符串类型数据。并且存储的数据仅在会话期间有效,也就是浏览器关闭后数据会被清除
- IndexedDB:异步存储,支持更复杂的数据结构,例如对象,存储空间较大,通常为数百 MB 甚至更多,还支持数据库中常见的事物等特性,适合存储大量数据和复杂数据。
考虑到当时我们项目的存储场景,存储的是表单控件的值,并非多大的数据量,然后重新打开标签页时值要回填,所以我选择了使用 localstorage 来存储那些表单项的值。(解释了为什么选择localstorage)
存储这一块儿的处理倒不难,但也算是整个模块里面的核心功能之一。另外一个核心功能就是跨标签页数据通信,我这边选择的是 BroadcastChannel 来实现的。(钩子🪝)
# 3. BroadcastChannel
模拟问题:说一下跨标签页数据通信常见有哪些方案?你为什么选择了 BroadcastChannel ?
问题分析
- 常见跨标签页通信的方案有哪些?各自的区别是什么?
- 为什么选择 BroadcastChannel ?
- 设置下一个钩子
参考答案
跨标签页数据通信的方案也很多,像:
- localstorage + storage 事件
- 特点:通过 localStorage 存储数据,利用 storage 事件在不同标签页之间同步数据变化。
- 优点:浏览器兼容性好,使用简单。
- 缺点:只能在同源页面之间通信,且数据只能是字符串类型,storage 事件仅在不同标签页间有效,同一标签页的同域页面间无法触发。
- service workers
- 特点:可以在后台脚本中实现消息的接收和发送。
- 优点:可以实现更复杂的逻辑,如离线处理、网络请求拦截等。
- 缺点:实现较为复杂,需要 HTTPS 支持,初次设置和调试成本较高。
- postmessage
- 特点:通过 window.postMessage 在不同窗口或 iframe 之间发送消息。
- 优点:支持跨域通信,可以在不同来源的页面之间发送消息。
- 缺点:需要手动管理消息的接收和处理,适合窗口间或 iframe 间的通信,标签页间通信需要通过父窗口代理。
- broadcastChannel
- 特点:通过 broadcastChannel 实现多个浏览器上下文(标签页、iframe、窗口)之间的消息广播。
- 优点:使用简单,API 友好,支持多标签页、iframe 和窗口之间的即时通信,不受同源限制。
- 缺点:浏览器支持情况较新,不支持所有旧版浏览器。
因此综合考虑了各种方案的优缺点后,我选择了 broadcastChannel,这个 API 现在的兼容性也比较好,大多数浏览器都支持。而对于不支持这个 API 的浏览器,可以通过回退机制使用 localstorage + storage 事件的方案。(体现个人做事儿的风格)
这两个核心功能处理完后,基本上整个模块也就完成了,剩下的就是测试和发布了。(钩子🪝)
# 4. 测试与压缩
模拟问题:说一下你在模块发布前,还做了哪些准备工作?
问题分析
- 做了什么测试?使用什么来做的这个测试
- 使用什么工具进行的打包?为什么选择的这个工具?你的考量是什么?
- 设置下一个钩子
参考答案
首先就是我刚才所说到的测试,毕竟是要对外发布的一个公共模块,必须要确保模块的可靠性和稳定性,因此测试是非常有必要的。我主要对 FormStorage 模块进行了全面的单元测试。测试的内容包括:
- 基本功能测试:验证数据的保存、恢复和删除功能是否正常。
- 跨标签页同步测试:检查在不同标签页间数据是否能够即时同步。
- 防抖机制测试:确保在用户输入时数据不会频繁保存,而是等待一定时间后再进行保存。
- 边界情况测试:例如浏览器意外关闭、异常数据格式等情况,确保模块能够处理这些异常情况。
使用的测试框架是 vitest,这是一个轻量级、快速的单元测试框架,与 vite 项目高度集成,能够快速进行测试和调试。
另外一个就是打包与压缩,我选择使用的是 rollup,一般在打包公共库的时候用 rollup 要好一些,因为 rollup 有这么一些特点:
- Tree Shaking:可以自动去除未使用的代码。这对于公共库特别重要,因为它能够确保打包后的代码尽可能小,从而提高加载性能和使用效率。
- 输出格式多样:rollup 支持多种输出格式,例如 CommonJS、ESM、UMD 等。这使得一个库可以在不同的运行环境(Node.js、浏览器、模块打包器等)中使用,增加了库的兼容性和灵活性。
- 插件系统:rollup 提供了丰富的插件生态系统,可以方便地进行代码转换、压缩、文件处理等操作。例如,我使用了 @rollup/plugin-node-resolve 和 @rollup/plugin-commonjs 插件来处理模块解析和转换,并使用 rollup-plugin-terser 对代码进行压缩,确保生成的包体积尽可能小。
- 代码优化:rollup 能够对代码进行细粒度的优化,例如去除重复的依赖、压缩代码、内联代码等。这些优化能够显著减少打包后文件的大小,提高代码的执行效率。
这些优点使得 rollup 成为打包公共库的理想选择。这些准备工作做完后,差不多就可以发布了。
# 5. 包的发布
模拟问题:最后说一下你包的发布呢,你发布了哪些东西?
问题分析
- 发布的是源码还是打包后的代码?根据平台不同而不同
- 白名单和黑名单
- 收尾
- 要么突出个人做事儿风格
- 要么突出项目的收获
参考答案
不同的平台,发布的内容是不太一样的。
例如如果是 github 的话,主要是做源码分享的,所以准备工作这一块儿要多一些,涉及到 CHANGELOG、README 等各种文档。
而如果是 npm 的话,主要是做包的发布,所以一般需要做的就是对代码打包压缩,然后发布的是打包后的代码到 npm 仓库。
我这边主要是发布包到 npm 上面,因此要发布上去的是打包后的代码,这里还要对 package.json 文件进行配置:
- 配置包的不同环境下的入口文件
- 配置要上传的文件和目录
另外配置要上传的文件和目录也有两种方式:
- 白名单:files
- 黑名单: .npmignore
这里推荐用白名单的方式。这样即便后面新增了文件,白名单那一块儿也不需要做任何的修改。
通过封装这个公共模块,我对工程化相关的知识也比之前更加熟悉了,特别是 package.json 里面各种配置项,以前其实不怎么关心的,熟悉的就是 devDependencies 和 dependencies. 但是这里涉及到要发布包,所以针对入口文件配置呀、白名单配置呀、都有了更深的了解和掌握,这也算是我做这个项目时一个非常大的收获。
-EOF-