信号机要点
项目背景
简单的说是一款信号灯设备的配置平台, 主要功能是配合信号机设备对路口信号灯进行参数配置, 并结合其他硬件设备进行数据采集和上报, 并将结果以渠化图的方式呈现出来
主要的使用人员为 负责配置的技术支持人员 和 负责监控的交通事业单位人员, 属于内网项目
从使用者的角度分析, 项目主要关注的点是如何降低操作复杂性, 还有提高场景的还原度和实时性, 具体体现为
引入ThreeJs实现三维场景的渠化图绘制
ThreeJs自带坐标系、建模与交互能力, 比canvas开发维护成本更低
相较于DOM实现, 不需要频繁操作DOM, 性能和用户体验更好
使用websocket进行通信交互
服务端主动推送
相较于http报文, websocket传输的数据更小更快, 参考http报文制定了交互的协议标准, 并对传输数据进行了加密
要点记录
1、Vue 与 ThreeJs的结合 (涉及到的思想:双向绑定、事件发布订阅、diff比对)
Vue是数据驱动的MVVM框架, 会在初始化时对传入数据进行递归式的数据劫持
ThreeJS场景实例是一个庞大的树形结构, 不可以直接挂载在Vue实例下(因为会创建大量冗余的依赖对象), 提供了render方法将场景对象绘制到canvas中
本例中的做法是参考Vue的双向绑定, 将ThreeJs场景实例看做一种特殊的DOM操作
数据 -> 视图:
场景对象就是我们的虚拟DOM, 当Vue实例中的数据变化后, 找出最小的修改部分, 据此修改场景对象
场景对象到canvas画布的渲染过程由循环调用 requestAnimationFrame 实现, 原因有二
(1) 使用控制器移动相机位置时, 也需要重新render, 虽然可以同时监听数据变化和用户事件来决定render时机, 但是这样做会降低代码的可读性和可维护性(比方说依赖的数据扩展了)
(2) ThreeJS 内部就有优化机制, 仅实例的 needsUpdate 属性变化时才会重新计算, 因此场景对象没有改变时, 一次render的周期非常短(实测1~2ms, 修改场景对象时可达到50ms)
视图 -> 数据:
基于用户当前状态为canvas注册DOM事件, 基于Three的射线机制拿到交互模型在场景对象中的实例节点, 获取节点携带的数据(userData), 修改Vue实例中的数据
2、场景的保存与还原
为了降低操作的复杂性, 本例牺牲了一部分操作自由度, 即不允许用户对模型进行拖拽或旋转等操作
本例将交通路口抽象成最大八方向的底图模型, 每个方向对应一个路口模型, 由用户选择启用某几个方向的路口, 形成路口类型
路口模型下的子模型的坐标都是相对于路口模型来计算的, 这样做的好处是不需要记录每个模型的坐标, 通信的数据量被降到很低
路口模型下挂载多个车道模型, 每个车道模型都有自己的类型和所处路口的方向, 所以仅需要遍历车道数组即可还原底图模型, 其他的模型流程类似, 需提供自身类型与所处方向
3、业务模块的解耦
本例设计了一个基于用户操作的状态机, 将用户状态划分为 正常状态、(选中物体状态)、预备添加状态 等等
在状态切换的时候修改画布绑定的监听事件, 并且广播出事件, 触发Mesh实例注册的监听事件, 这应该是一个观察者模式, 如此可以避免对模型的操作堆积在状态切换的代码中
每个业务模型都被单独作为一个类, 本质是一个树形Mesh实例, 所以本例改写了Mesh的原型链, 为其赋予更多通用的业务功能, 包括向状态机注册监听事件
4、性能优化方面的实践
(1) 场景对象挂在window下导致的内存溢出问题
问题产生的原因: 创建模型实例时, 其依赖的几何体实例、材质实例、贴图实例等会重新创建, 但删除模型实例时, 其依赖实例没有一并删除
新增模型时, 采用克隆的方式复制其依赖, 这可以复用依赖的依赖, 减少新内存的开辟, 但同时也可能造成修改模型的时候误修改其他模型, 所以需要根据业务选择克隆的范围
删除模型时, 需要递归访问该节点下所有依赖节点, 手动触发销毁事件
(2) 适当降低模型的线面数
(3) 数据变化时, 减少需要修改的场景对象范围, 具体体现在数据变化时, 与上一次数据进行比对
如果是路口类型的变化, 则取交集, 删除不可复用旧路口模型, 新增未创建的路口模型
如果是车道数量的变化, 则仅删除路口模型下的车道模型, 重新添加
(4) 使用 requestAnimationFrame 进行渲染操作, 由浏览器决定执行时机, 降低丢帧风险
5、通信模块的封装 (关键词:发布订阅、文件处理、web-worker)
采用发布订阅的机制, 业务模块调用通信模块接口传入回调, 注入对象形式的回调池, 键为客户端生成的请求唯一码, 值为回调函数
在onmessage中, 根据服务端返回数据中的请求唯一码查找并触发回调, 如果是单次回调则将其从回调池中删除, 如果是持久回调(主动推送)则不删除
6、通信模块的协议制定
参考http报文, 协议报文为长度为2的数组, 第一位是协议头(包含url,类型,请求唯一码), 第二位是协议内容(json格式的字符串)
7、鉴权方案与断线重连(本例的瑕疵之一, 密码存在客户端是一个隐患)
服务端根据唯一值来管理连接, 过滤每一次请求判断连接是否已经授权, 唯一值在服务端生成, 客户端在登录前获取, 并在登录时返回给服务端
登录前, 从服务端获取密码加密方式(sessionID, 加盐随机串, 迭代次数), (md5+随机串)x指定次数的sha256, 生成不可逆的字符串
登录后, 将 用户名 和 AES加密的密码 存入 sessionStorage, 用于断线重连
8、数据加密的处理
采用的是对称加密, 特点是加密速度快, 但是将对于非对称加密安全性降低(密钥泄露问题)
9、文件上传的处理
使用 FileReader 读取表单中的文件实例得到 ArrayBuffer, 再转成可操作的 二进制数组 Uint8Array
此时可以读取头部的二进制片段判断文件类型(相同类型文件的头部都是固定的, 这比判断后缀名更加可靠)
使用MD5进行完整性校验, 当文件较大时计算时长较长可能导致浏览器暂时失去响应, 可以将其转入web-worker进行, 不抢占JS主线程
切片与断点续传: 拿到可操作的二进制数组后可以进行分割, 在切片头插入当前切片在文件内的起止点与长度


查看5道真题和解析