# 总:架构的结构 1. [架构的定义和范围](#scope-%E6%9E%B6%E6%9E%84%E7%9A%84%E5%AE%9A%E4%B9%89%E5%92%8C%E8%8C%83%E5%9B%B4) 1. [技术栈](#tech-stack-%E6%8A%80%E6%9C%AF%E6%A0%88) 1. [数据结构](#data-structure-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) 1. [服务的接口](#service-%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%8E%A5%E5%8F%A3) 1. [状态管理设计及数据流设计](#state-and-data-flow-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86%E8%AE%BE%E8%AE%A1%E5%8F%8A%E6%95%B0%E6%8D%AE%E6%B5%81%E8%AE%BE%E8%AE%A1) 1. [目录结构](#folder-structure-%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84) 1. [核心工具接口](#core-utils-interface-%E6%A0%B8%E5%BF%83%E5%B7%A5%E5%85%B7%E6%8E%A5%E5%8F%A3) 1. 开发规范(api/component/container/css) 1. [前端 URL 规划](#page-urls-%E5%89%8D%E7%AB%AF-url-%E8%A7%84%E5%88%92) 1. [路由、页面划分、子组件、常量](#router-%E8%B7%AF%E7%94%B1%E9%A1%B5%E9%9D%A2%E5%88%92%E5%88%86%E5%B8%B8%E9%87%8F) 1. [环境变量设计](#environment-variables-%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E8%AE%BE%E8%AE%A1) 1. [constants 设计](#constants-constants-%E8%AE%BE%E8%AE%A1) 1. [开发环境设计](#dev-settings-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E8%AE%BE%E8%AE%A1) 1. [API 处理设计](#api-status-code-api-%E5%A4%84%E7%90%86%E8%AE%BE%E8%AE%A1) 1. [network request: 网络请求设计](#network-request-%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E8%AE%BE%E8%AE%A1) 1. [消息提示处理设计](#notice-handle-%E6%B6%88%E6%81%AF%E6%8F%90%E7%A4%BA%E5%A4%84%E7%90%86%E8%A7%84%E8%8C%83) 1. [错误处理设计](#error-handle-%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E8%AE%BE%E8%AE%A1) 1. [样式管理设计](#styles-design-%E6%A0%B7%E5%BC%8F%E7%AE%A1%E7%90%86%E8%AE%BE%E8%AE%A1) 1. 行为统计设计(百度统计) 1. [日志设计(本地加密日志与线上日志)](#logging-%E6%97%A5%E5%BF%97%E8%AE%BE%E8%AE%A1%E6%9C%AC%E5%9C%B0%E5%8A%A0%E5%AF%86%E6%97%A5%E5%BF%97%E4%B8%8E%E7%BA%BF%E4%B8%8A%E6%97%A5%E5%BF%97) 1. 故障诊断设计 1. 弹出层设计 1. 组件复用设计 1. 定时器设计 1. 人脸识别工具设计 (新加) 1. [缓存和 PWA 设计](#cache-and-pwa-%E7%BC%93%E5%AD%98%E7%9A%84%E8%AE%BE%E8%AE%A1) 1. [JS 代码混淆设计](#js-obfuscation-js-%E6%B7%B7%E6%B7%86%E8%AE%BE%E8%AE%A1) 1. Electron / browser 检测与开发运行设计 1. Electron 打包设计 1. websocket 设计 1. 摄像头处理设计 1. 时间同步设计 1. [Layout 设计](#layout-%E8%AE%BE%E8%AE%A1) 1. 限流设计 1. 编辑器设计 1. [防作弊设计](#prevent-cheating-%E9%98%B2%E4%BD%9C%E5%BC%8A%E8%AE%BE%E8%AE%A1) 1. [局部技术难点](#tech-challenges-%E5%B1%80%E9%83%A8%E6%8A%80%E6%9C%AF%E9%9A%BE%E7%82%B9) 1. [技术特性验证](#tech-verify-%E6%8A%80%E6%9C%AF%E7%89%B9%E6%80%A7%E9%AA%8C%E8%AF%81) 1. [升级策略](#upgrade-strategy-%E5%8D%87%E7%BA%A7%E7%AD%96%E7%95%A5) 1. [选做功能](#optional-feature-%E9%80%89%E5%81%9A%E5%8A%9F%E8%83%BD) 1. [Others](#others) ## Scope: 架构的定义和范围 设计是为了将困难解决在设计阶段,同时为预估工期提高准确度。 适合有点复杂度,追求高质量的项目。 设计目标: 1. 混淆。(可以增强,但是 2 周以后被破解了呢?)动态更新防破解,组合式的手段。 2. 可维护性。变高。 3. 遗留问题,得到处理。 4. 体验优化。提升速度,消灭页面瑕疵。 5. 扩展性增强。 为保证设计不快速过时,设计文档仅记录不太变动的核心数据结构和流程,部分重要细节。 某些部分记录下来,也仅仅是为了帮助初次开发,后期如果过时了,可以写明已过时不再维护了。 组件设计;仅为辅助设计的脚手架,后续不维护? 开发环境设计(基础设施、文件目录、readme):不是设计的核心内容,确实将设计实现的重要入口 ## Tech stack: 技术栈 预计采用的技术栈和工具链如下 1. Vue 3.2 1. and-design-vue 3.x or naiveUI 1. pinia 2 1. vue router 4.x 1. typescript 4.5 1. eslint 8 1. prettier 2 1. vite 2.x 1. javascript-obfuscator 2 ## Data structure: 数据结构 ```ts // 详见:student-client.d.ts type Store = { /** 当前用户 */ user: { id: number; /** 身份证号 */ identityNumber: string; /** 学号 */ studentCodeList: string[]; // ... }; // ... }; /** 得到当前客户端版本 1.9.* */ type GetCurrentClientVersion = () => string; type IsSupportedClientVersion = () => boolean; ``` ## Service: 服务的接口 写明层级的目标,设计层级的协调功能(接口)。 tools layer: 工具类,可以脱离项目的函数 api layer: 通信类,处理和服务端数据的交互 service layer: 核心服务类,处理项目的生命周期 data binding layer: 提供给 UI 展示的类 user event handler layer: 提供处理用户触发事件的类 ## State and data flow: 状态管理设计及数据流设计 单向数据流与复式记账法的比较。 数据的处理原则要遵循以下原则: 1. 集中统一 1. 命名清晰 1. 类型与内容一致 1. 不包含重复数据 1. 上层清理数据,比如网络获取 JSON,里面的数据类型不对,要清理以后交给下层 组件的分类(适合复杂业务): 1. container component: fetch data 1. presentational component: display data ### 以下待整理 不涉及到 store 的状态,直接通过 api 请求,当做 component state 来使用。 好处:减少状态共享。 当一个状态被创建出,要明确它的 owner/scope/lifecycle,以及可能的值。 当一个状态被使用时,要明确它可能的值,做好防御性编程。 vue component wrapper. wrapper 里面放权限之类的异步获取数据。comp 里面放业务逻辑和同步的 props。 或者用 vue router 的 before guard 疑问:究竟是在 action 里面还是 container 里面请求数据?依据是什么? store actions -> state/storage -> page state: page refresh? => sessionStorage (退出登录后要清除部分字段) 错误要直达页面元素呢?-> action return data 好处:页面清爽;数据集中处理 数据的共享与过期 page 上的数据满足以下几点: 1、对数据仅仅做展示 2、数据输入来源为 URL(path & query) 上个页面传递过来的数据(为保证刷新可用,最好通过 URL 做简单的数据传递) 本地存储的数据(一般不是和页面主动关联,是通过 URL 或其他事件导致要取本地存储) 组件生命周期 用户输入 原生事件(网络) ## Folder structure: 目录结构 文件名/文件夹 组织模块用 强联系的文件放在一起 文件名有区分度,不要过分依赖文件路径名。否则会出现很多像是 index.js 这样的文件。 文件影响范围:不超出文件夹最好,image 或 package private ```sh . ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── prebuild.mjs ├── prettier.config.js ├── public │   └── favicon.ico ├── src │   ├── App.vue │   ├── api │   │   ├── loginPage.ts # api 请求 │   ├── assets │   │   └── logo.png │   ├── components # 页面其中的组件,可能复用 │   │   ├── PageError404.vue │   │   ├── QmButton.vue │   │   ├── QmDialog.vue │   ├── constants │   │   └── constants.ts # 项目中的常量。url, name, code, │   ├── devLogin.ts # 开发模式下自动登录 │   ├── devLoginParams.ts │   ├── env.d.ts │   ├── features │   │   ├── Login │   │   │   ├── Login.vue │   │   │   ├── images │   │   │   │   └── bg.png │   │   │   └── use │   │   │   └── \*.ts # 抽象页面的业务逻辑 │   ├── filters │   │   └── index.ts │   ├── main.ts │   ├── plugins │   │   ├── axiosApp.ts │   │   ├── axiosIndex.ts │   │   ├── axiosNoAuth.ts │   │   ├── axiosNotice.ts │   │   └── eventBus.ts │   ├── router │   │   └── index.ts │   ├── setups │   │   └── useTimers.ts │   ├── store │   │   └── store.ts │   ├── styles │   │   ├── cssvar.css │   │   ├── global.css │   │   ├── nprogress.css │   │   └── tailwind.css │   ├── types │   │   ├── 3rd.d.ts │   │   ├── global.d.ts │   │   ├── index.ts │   │   └── student-client.d.ts │   └── utils │   ├── renderJSON.ts │   ├── ua.ts │   └── utils.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ``` ## Environment Variables: 环境变量设计 PORTABLE_EXECUTABLE_FILE:electron 添加,运行时获取可执行文件的路径 VUE_APP_SKIP_CHECK_NATIVE:允许本地通过 web 来访问学生端服务 VUE_APP_CORE_HOST_URL:本地开发时,后台服务器的地址 VITE_SLS_STORE_NAME:阿里云日志服务地址 VUE_APP_CONFIG_FILE_SEVER_URL=https://ecs-static.qmth.com.cn VUE_APP_GIT_REPO_VERSION=TO_BE_OVERRIDED # 显示版本号 ## Constants: constants 设计 ```ts export const REMOTE_APPS = [ ["qq", "QQ"], ["teamviewer", "TeamViewer"], ["lookmypc", "LookMyPC"], ["xt", "协通"], ["winaw32", "Symantec PCAnywhere"], ["pcaquickconnect", "Symantec PCAnywhere"], ["sessioncontroller", "Symantec PCAnywhere"], [/sunloginclient/gi, "向日葵"], [/sunloginremote/gi, "向日葵"], [/选择免安装运行,截图识别/gi, "向日葵"], ["wemeetapp", "腾讯会议"], ["wechat", "微信"], ] as const; ``` ## API status code: API 处理设计 api 的 statusCode: 1. 200:正常 1. 401:无权限 1. 403:无权限 1. 503:后台限流,要求重试 api 请求是否显示错误: noErrorMessage: boolean (某些情况下希望网络请求静默处理) 当 statusCode 为非正常时,优先显示 desc 字段,如果没有,则显示“未定义异常:......” “websocket 重连失败” => "服务器连接失败(websocket)" ## network request: 网络请求设计 网络请求可以限流、重试、取消。 ### 给进行中的网络请求覆盖全局的遮罩层 网络请求可设置遮罩层,防止请求过程中用户的误操作、重复操作等等。有 mask,拒绝键盘事件。 ``` globalMaskCount === 0 ====> no mask log: inc/dec globalMaskCount, reason ``` loading / error 状态不进 store 但是可以通过请求的状态机来处理? ### network reactive design(待细化) const { data, isLoading, isFinished, error } = useAxios('/api/posts') 好处/难处: 1. v-loading="isLoading" 2. data 模板中直接用?因为有 loading 开关 3. 请求有依赖?不好搞? 4. 错误处理不好搞? 5. 重复请求? network loading state from api and reactive? get reactive network result? form inputs / confirm / throttle? / error => network params => res/loading => display result 多个 loading 状态转换? ## Error handle: 错误处理设计 错误的边界是用户,无论怎么处理,错误的不可避免的,一定要通知到用户,或不给用户错误的结果(先成功后失败)。 错误分为两种,一种是给用户看的错误,一种是给程序排错或恢复的错误。 错误应该抛出 Error 对象,不能 throw 字符串等类型。 ### 用户查看类错误 1. 错误以弹窗形式提醒用户。 1. 错误信息应有识别度,方便用户反馈。 1. 错误应隐藏敏感的技术信息,比如使用的技术种类。 1. 重试时应该不报错,默默重试,但要记日志。 ### 程序排错类错误包括: 1. JS 错误,需预警,保证及时处理 1. 网络错误,记日志 1. 环境错误,提示用户,记日志 1. 兜底的错误记录 ### 网络的错误处理: 1. 补充后台的错误码。如果后台没有统一的,则只能模糊处理。 2. 未知错误(待讨论):a、显示错误的详细信息 b、错误监控 c、不显示详细信息(不能定位错误) ### 潜在错误位置 1. 在 vue 组件中,某些方法可能会在 vue 组件被销毁后执行。 1. 错误内部处理被特殊压制?定制错误处理,axios config ? 1. 发生错误后,流程逻辑是否正常。 ### Promise 的错误处理实践 1. 将长串的流程拆解成多个 promise,利用 promise 来处理错误嵌套? 1. 将 promise 嵌套超过两层的抽象出来变成 async 函数 1. early return ?? 抽到流程的第一层;throw 不被处理从底层抛到高层;中间层一般不处理底层的错误,而且可能自己 throw guardError; 1. 错误内部处理?顶层不知道用户友好的错误 => 如果追求这个体验,那么有大量的消息在中间层传递。大部分软件是不追求这个的。 ### reference 1. [JavaScript 错误处理完全指南](https://mp.weixin.qq.com/s/I9ZrCsoNo7jrOHj8a9UW1A) ## Logging: 日志设计(本地加密日志与线上日志) 同时记录到本地日志,以及网络日志,网络日志部分可以做监控提醒。 本地日志要采用加密算法,同时在系统处于登录界面时,将本地日志上传。 日志的主要内容为: 1. 可识别用户 1. 环境信息(硬件、网络、OS、process) 1. 核心流程的操作 1. 错误信息 ### 日志分等级记录 1. debug,收集个别考生的全日志信息?如何设置它?(ctrl 5 秒内连按 5 次,启动 debug 日志级别) 1. log,全流程日志。 1. warn,疑似错误信息。(low network speed) 1. error,错误信息。(硬件错误,文件不存在) logger => (destination: baidu / console / file / aliyun) (type: error / warning / operation) (easy / format / single / conf) ### 打印日志的注意事项 1. 打印一个 JS error 对象? ===> new Error('err').stack (IE10+) 2. Promise 的异常通过 Promise.reason 来获取 ## Page URLs: 前端 URL 规划 [前端 URL 规划](https://doc.qmth.com.cn/pages/viewpage.action?pageId=23330835) 1. https://ccnu.exam-cloud.cn/oe-web/index.html 1. https://ccnu.exam-cloud.cn/oe-web/{js, css, img, fonts, service-worker.js}/\* 1. https://ccnu.exam-cloud.cn/admin/index.html 1. https://ccnu.exam-cloud.cn/admin/{js, css, img, fonts, service-worker.js}/\* 1. https://ccnu.exam-cloud.cn/photo-upload/index.html 1. https://ccnu.exam-cloud.cn/photo-upload/{js, css, img, fonts, service-worker.js}/\* 1. https://ccnu.exam-cloud.cn/oe-wap/index.html 1. https://ccnu.exam-cloud.cn/oe-wap/{js, css, img, fonts, service-worker.js}/\* 1. https://ccnu.exam-cloud.cn/api/* ## Router: 路由、页面划分、常量 学生端是属于严格受控的页面跳转,既考虑刷新,也考虑缓存。 要禁用链接的拖动或通过按 Ctrl 点击链接多开窗口。 ### 页面划分 1. Login 1. MainLayout 1. OnlineExamContainer 1. .... 1. ModifyPassword 1. ExamingHome 1. ExamEnd ## Core utils interface: 核心工具接口 clientVersion.ts 管理可用客户端版本。 api: getCurrentClientVersion isSupportedClientVersion updateManager.ts 应用(非客户端)是否应该更新了 api: isNewWebAppAvailable isNewBackendAvailable getNewBackendDesc native.ts 管理原生及系统命令,详细说明 path 和相对 path api: isElectron execCmd readFile getRemoteApps getVirtualCams runtimeMonitor.ts 检测当前运行的环境是否合法 检测是否打开了控制台;检测是否使用了高版本的 chrome(feature 检测) 检测 onResize;截屏 todolist: 1. md5 文件校验工具方法 1. auth 认证 1. 工具类的测试 ### Layout 设计 仅有一个 MainLayout 供复用,在 router 里面设置 layout。 - MainLayout: - SideBar - Header - MainContent - MainContent 不包含统一的 margin,但为了方便,提供全局的 class 来设置 MainContent 的 margin. ## Styles design: 样式管理设计 全局的样式包含以下功能: 1. reset style 2. 引入组件库的 style(看能否按需引入) 3. 全局的字体 4. 全局的 css variables(颜色、字体大小) 5. 全局的 class。title-text。primary-text。secondary-text。comment-text。 组件内部的样式应采用 scoped,将样式限制在组件内起作用。 ## Notice handle: 消息提示处理设计 提示的种类:成功、警告、失败、进度 提示不能过于频繁,要对重复的消息提示做限流处理。 部分流程性质的消息提示,可直接更新内容,延长显示时间。比如“正在上传文件”,“上传成功”。 ## Cache and PWA: 缓存的设计 检查各个服务器资源的缓存设置: 1. 后台服务器的 HTML/JS/CSS/image 2. 阿里云上的 studentClient.json / backgroundImage. service worker cache html/js/css/image 保证首页快速可用。 保证在运行过程中,可以更新到最新版本的 js。 缓存分为应用层和网络层。 应用层的缓存主要是将 store 缓存在 sessionStorage。 都要注意缓存失效的问题,比如用户点击退出回到登录界面,通过权限获取的缓存就应该失效。 PWA 最主要的功能是 cache cache 要判断: 1. 对于更新不敏感的资源体验最好 2. 注意缓存失效的策略 3. 名字作为缓存的名称 4. 页面 html 作为缓存的入口,这里是不确定的,但它包含的资源可以通过名称来跟随 html 是否缓存 5. 网络请求的缓存。post 的不应该缓存。get 可以缓存,刷新并且网络是通畅的情况,请求新的网络请求来了,那么可以替换。 6. 手动清缓存。做一个按钮,让用户手动点击,unregister service worker。 service worker ; image cache; audio cache/prefetch ## dev settings: 开发环境设计 再使用了 vite 来作为开发服务器后,还有几个可以提升开发效率的功能需要研究。 http/ws proxy 开发模式下: 设置一个服务器 ip 地址,即满足所有的 https/wss 转发。 设置学校域名 设置是否为测试环境。最好能 preval,减少代码信息泄露。参考 preval.marco / ts-transformer-preval ## JS obfuscation: JS 混淆设计 ### tools 1. rollup.js 1. js-obfuscator ```js { rotateStringArray: true, // 防止打开devtools,打开后程序会冻结 // debugProtection: true, // debugProtectionInterval: true, // // 防止代码格式化 // selfDefending: true, // // 要测试sourcemap是否还有用,以下sourcemap配置不起作用 // sourceMap: true, // sourceMapBaseUrl: "http://localhost", // sourceMapFileName: "exam", // sourceMapMode: "separate", splitStrings: true, // stringArrayEncoding: ["none", "base64", "rc4"], } ``` ### 消极影响: 查错会很困难,会变慢。 Todolist: // sessionStorage design // store 防破解?仅保存 user, QECSConfig 信息 ## 前端加密解密设计 前后端协商加密解密,避免接口敏感信息被暴露。 1. 前后端协商 N(比如 5)种加密解密方式 2. 后端选择加密的组合方式,1 到 2 种进行连续加密 3. 前端在需要加密的接口使用后端规定的加密方式进行连续加密,参数要加上时间戳 4. 后端验证通过后,返回的 response 也是用之前加密的方式加密过,前端再进行相应的解密 5. 加密方式被禁用后,仅影响开考接口? 6. 后端对加密方案的选择:间隔一段时间才换加密组合 ## Prevent cheating: 防作弊设计 1. 检测伪装 Chrome 版本号的行为 - 检测 Chrome 高能力特性 ## tech challenges: 局部技术难点 技术难题: 1. iframe 的摄像头等数据共享和通信 1. 学生作答编辑器 1. 音频播放器 1. 摄像头各种错误捕捉和提示 1. webpack / babel 提前执行 js,减少代码长度。确认开发环境不检测 native 的代码。 1. websocket as a library 1. network with / without auth 1. performance 1. event design: 作为解耦核心流程的手段,用来反调试 ## Tech verify: 技术特性验证 - [x] Electron 1.7.16 支持 Proxy / CSS var 等。 - [ ] naive ui 核心组件在 Electron 的适配 - [ ] 开考、交卷接口由于接口超时被重复调用,会发生什么? - [ ] query 的最佳实践? 1. 如果有不需要链接被分享、收藏、日志或被直接跳转进来,prefer state。typed state? 1. 传确定的简单数据。类型转换怎么做? - [ ] select 初始值类型的研究 - form 字段 null/undefined 的最佳实践 query params: 1. select,未选择,默认为 undefined? 默认为 undefined,则可不传递到后台. clearable => undefined 2. input,未填写,默认为 undefined,查询不传递。如果后台有需要,可以通过 watch 将‘’转 undefined 3. 在 axios paramsSerializer 中处理 query parameter, https://github.com/axios/axios/issues/1139 https://www.npmjs.com/package/qs paramsSerializer QS defaults: null => '' ; undefined => omit ; change arrayFormat 'comma' ## Upgrade strategy: 升级策略 新增加 API 支持新前端,后端维护多套 API,一次升级。 前端保持多套,新旧共存。 nginx 通过域名来分发新旧前端代码。 如此,可支持前端灰度升级。 ## Optional feature: 选做功能 1. Electron crash 日志捕获及上传 1. 不适用的宿主环境日志 1. Electron 启动速度 1. 非生产环境,右上角显示 tip,并可以打开 console 1. Electron 加固(替换网络层,用 C 来发网络请求,探索可行性) 1. 先得到抓拍的 png 照片,再通过 canvas 得到 jpeg 格式的照片。[js image manipulation](https://github.com/oliver-moran/jimp) ## 待整理 ======测试规范===== 工具类充分测试 服务类模拟测试 前端单元测试:dom 是否如期变化,api 是否发出 script 抽出来 business logic code? 不好抽取,因为大部分和 UI 相关。将 UI 相关的类抽取成不依赖 UI 的属性集合比如 ImageLike. ======测试规范 end===== ======校验规范===== 校验字段的 UI 显示 必须校验 校验消息提示 Form rules validator 校验的问题在于:代码太多,重复太多,干扰正常逻辑 ======校验规范 end===== ======Mock 的研究====== api mock? 1. 基于 api 规范。 2. 随机生成数据。 easy mock 线上版。 easy mock 线下版。可以迁移。 基本满足了需求。 mock 的优缺点? 缺点: 1. 内网更新比较麻烦 2. 有转换成本 ======Mock 的研究 end====== ## Others ======设计的遗留问题和感悟===== 前端架构的责任: 1. 架构是方向性的。提出目标和方向,无论底层技术和框架如何发展,目标和方向都不会变,甚至能更好的利用到变化。 2. 架构是管理变化的。当可能的变化点来临,架构能够处理。 3. 架构是划分权力和责任的。即不同的人员负责不同的模块和层级,互相有大致明确的依赖和边界。 4. 架构是负责增长的。 5. 架构是深刻理解了底层技术和业务需求,搭建出的架构既稳固又灵活。 _. 方向性:易使用、易维护、高性能、高兼容、易调试、日志完善、安全性、文档、code review。。。前端工程化 _. 日志完善 _. 作用域可控 _. 接口规范 _. 数据类型 => IDE 编程 _. 测试 设计一个项目时: 1. 先垂直切分,分为几层 架构:关注生命周期,切割生命周期,关注模块,关注模块间的通信 工具:将常用操作收敛在一起 业务:具体细节 几种设计方式: 1. obj.method 耦合,同步 2. postEvent & obj 松耦合,异步,但程序更抽象,必须把模块设计好 3. store & emit 共享数据,模块耦合程度更低 看得远,才能做设计。 如何看得远?经验,想象力,推理能力。 几种程序的架构方式: 1. app start/register module 2. plugin callback 3. component nest 前端架构:减少重复劳动。库、组件库、流程制定、高难度工具。 先开发逻辑?还是同时开发样式? 先设计通用组件?还是后面提取? 程序设计能力和过程是估工期的重要因素。 架构与收纳: 方便获取(就近) 方便记忆(分类摆放) 容易还原(将获取模式固定,避免系统状态杂乱) 流程抽象 设计程序时: 1. 分解流程(操作人的责任范围即为一个流程) 2. 制定接口(接口即为双方开发、调试时查找问题的边界) 3. 保留日志(接收的输入,返回的输出) 4. 错误处理 看源代码: 1. 模块关系 2. 模块加载 3. 模块初始化 4. 模块动态关系 架构: 1. 架构的核心工作是写出核心生命周期。由核心模块去实现核心生命周期。每个核心模块又有自己的生命周期。 1. 如何分出模块?模块划分的标准:包含一段核心流程。可以独立更新。保持接口服务的稳定性。复用是附加好处。 1. 如何一眼看出两个模块之间的关系。交互的数据量大小。 框架的目的: 1. 整体可控,整体流程和接口 2. 给模块保证接口,内部可发挥,可 hack,不影响整体的质量 结构良好的程序: 1. 总组件控制流程 2. 子组件控制细节 3. 总组件和子组件通过接口来沟通 结构不好的程序: 1. 总组件只是个入口 2. 细节和跳转逻辑都在每一个子组件中 有没有可能总组件无法控制流程? 如果父子、子子组件耦合过高,那么很难梳理出清晰的总流程。 MVC 分层与组织的树状图? 组织结构 CEO 下面总监。总监下面经理。经理下面员工。 总监层? 每一个层级的职责。目标、进度、指挥、协调。 编程时脑袋里应该存放什么信息(超越本能,练习专业能力): 1. 流程逻辑 2. 当前打开的文件,之前打开的文件 3. 预期结果是什么 究竟是前端编织业务,还是后台自行沟通? 组件: 1. 组件本身的 scope,作为一个对象,需要注册。 2. 组件的本身包含:模板、样式、逻辑。模板是内部的,也可以接受输入。由于 CSS 的特性,会受全局 CSS 的影响。JS 是内部的,接受外部传入的 props。 3. 模板接受外部的为 slot。 4. CSS 接受外部的 style 和 class。都是只接受当前层?其他改变样式的通过 props 传入。 5. JS 通过外部传入。包括事件回调。接口包括变量和事件。 6. 组件封装,主要是在业务层作用。以实现业务为主,根据业务特点来确定通用性,但不追求复用。 7. 组件复用主要是框架层。 8. 组件间通信。props 和 event。props 的耦合性更强,耦合并非坏处,逻辑上该紧耦合就紧耦合。 9. vuex。全局状态的分发。状态的更新方法和传递。 从需求推导框架的设计,最后到项目的架构: 用户需求: 1. 更快的响应 2. 更细致的体验 3. 更好的使用机器的特性,如下 4. 屏幕大小的适配 5. GPS,摄像头,通知 6. 由于数据的物理距离的亲密性,在设备内计算是必须的 环境变化: 1. 硬件更强,CPU 运算更快 2. 网络更好,带宽,延时都有进步 3. 需要考虑电池 4. 移动端的屏幕 5. 浏览器 API 更强大 6. 前端开发工具链更发达 通用框架的需求: 1. 降低前端开发的复杂性,如下 2. 渲染 => 将数据变成 HTML。模板。DOM 性能 3. 将渲染的流程优化。 a. 即模板不变,只是根据数据动态改变 HTML,和传递用户的输入。 b. 将逻辑集中到数据操作。因为数据逻辑更加易于测试和推导。 c. 如何监控数据?deep? native value? Array value? 4. 组件式开发。例子:UI 库,ant-design。可定制的部分?样式、行为。 a. 如何支持组件?核心问题。查看 Vue 的源码。 b. 从模板来的组件树。编译原理?? 5. 数据的流转。store,single source of truth. 数据的共享和隔离。不可变数据? 6. router。适合 web 的一种将应用模块化的方式。用来组织程序,分包,并给用户在程序中定位的能力。 原理:假如自己来写一个前端框架应该怎么写? 如何组织一个应用程序?分层。模块化。将应用程序模块化。 项目开始编码前问几个问题: 1. 有几个 module? 2. 是否需要划分 module?划分 module 的原则是,项目足够大,需要划分 module 来降低复杂度。未来有可能划分吗? 3. 划分模块后,store/routes 和 modules 放在一起 view 的具体划分 components 纯视图组件,可共用。比如 view libs,iview 之类的。 views 和 routes 相关的组件。带状态,最起码和 URL 相关。 组件划分的需求(终极目标降低复杂度): 1. 可被复用。如果很通用,非业务组件,就追求这一点。业务组件很难被复用。 2. 业务组件的目标。利于维护,需求来自于业务划分。 利于维护可细化为: 1. 分离相互可以独立的功能,每一个模块可以独立完成一个功能,即因为有了这个模块存在,项目的复杂度降低了。(树形划分功能,并将功能之间的关系连接) 2. 接口清晰。即功能之间的关系最小化。 数据类型: 全局公用一份,比如 user,privilege 衍生数据,通过 computed 来拿?getter 数据逻辑: 不同 module 的数据不共享,由各自的 module 去维护更新。如果 module 之间的数据不一致,可以设置全局标志位,或由页面刷新来保证一致性。 Modal 的数据有包含它的组件(Main)提供,Main 提供数据处理完的回调。 在 Modal 里完成对数据的校验和提交。 增加和修改的功能抽离为一个组件:所属的代码较多,和主功能牵扯较少。 组件与 store 数据之间的关系: 1. 尽可能减少使用 store 数据。优先使用 props 传递,这样组件更加独立。 2. 数据集中使用 3. 尽量少修改数据 4. 做好事件回调,将组件内发生的事情传递出去。 设计的原则(主要从人的大脑理解能力出发): 1. 顺序。一个系统切分为多个流程。流程又切分为子流程。流程主要是顺序执行的,较少判断跳转和循环,杜绝随意的流程间的连接。 2. 集中。具有相同性质的事务,应该集中处理。 设计一个系统就好像给一个团队分工,系统分工不必关心具体人的能力和性格,但是需要知道底层工具组件的能力。 甚至一个系统最终是给不同的人使用的,那么这些角色的操作流程、效率、配合和权限也要考虑在系统中。 系统的演化: 1. 混为一团。 2. 识别出主要模块,模块间连接没有规范,物理上还在一起。 3. 模块间通信规范,物理上没分离。 4. 模块物理分离。 https://www.infoq.cn/article/AJ0S3IDEHyusNms0bTf1 深度解读当代前端架构演进与趋势 MVC: 将 view 和 model 分开,这两个都是比较复杂的部分,且可以独立变化,并且各自的细粒度组件可以复用。controller 是粘合剂。 经典 MVC 时代,视图没有标准(图形操作系统还没有诞生),视图的输入输出比较底层,比如接收键盘输入,这个时候 controller 就做了大量非本质性工作。 controller update model. controller update view. event & reference 都被使用了。 Application Model: 某些状态是属于 view 的,但又不能放 view 里面,因为会影响 view 的通用性,同时这些状态又是 model 派生出来的,但理论上 model 与 view 是无关的,所以 model 来维护这些状态很奇怪。 比如超过某个数字,把 label 变成红色。 这时就创造出 AM 这个概念了。AM 来维护 view 的状态。 以上 MVC, 现代个人操作系统大大简化了输入输出,PC OS 一开始就为个人设计,所以 GUI 是重中之重,view 的功能强大了,却同时简化了,大部分 controller 要做的底层部分被简化掉了,变成了 view 内置的逻辑。 MVVM: data-binding Model 在哪里? view 是 template & style? MVVM => 重点在于 VM 是 data binding view,data 改变则 view 自动改变 MVVM => data(model) -> transform(view model) -> view MVP MVC (UI no auto update) 前端的变化:业务流程优化!以往 web 前端弱小的时候,流程是在服务端控制的多页面(业务窗口)完成的,现在前端强大了,虽然可能还是多页面,但已经是由前端控制的流程。 即由多业务窗口多次提交,变成统一业务窗口了。 改变开发的思维模式: 首先设想页面要展现什么数据(暂时不考虑组件) 假设数据已得到(并且要设计数据模型) 数据会如何变化 总是在第一次采用最直觉的写法完成任务,之后再优化性能和体验 比如对于易过期的数据,总是获取而不考虑缓存 关于 null 的思考 boolean select with null? and filter null? component: state: null; // default api pass state null/false/true? api: if pass null to api 是可以考虑的 search is null? search 时 null 的话,不传。 edit 时,null 必须要传到后台 (将 enabled 修改为 null) component prop forSearch? no search { enabled} 当传递到 api 时,去掉 null 前端开发的几大主题: 1. 校验 2. 异常处理 3. 数据流 4. 安全 5. 图形化 6. 数据模型 一个应用如果只考虑正确情况,那它的架构和状态应该是怎么样? 此为一个简化版本,应用的是第一性原理。 然后考虑一个应用如果考虑网络请求,网络错误,日志,性能,动画,复用,错误提示,校验? ======设计的遗留问题和感悟===== ======脚手架设计====== Vue 框架 avue 评估 混用 avue? theme? 样式覆盖 网络调用 vuex avue 的 vue 使用的是过时版本,它的长期发展不明确,还是自己维护组件比较靠谱。 q-crud: 增删改查 q-table: q-page: 脚手架快速启动: login page 4xx/500 page router store 权限/角色 只使用 avue 的 表单+表格+新增,分页,网络请求,错误处理 脚手架初始化公共功能 前端公共模块 提炼公共接口标准 提炼公共设计模式 vue 权限管理: 加入脚手架 scrollbar; visible/hide/width restore compatibility document.title i18n CRUD: paging, save page state? 最低要求,一个丰富的 sample 页面,增删改查分页上传。最高要求:配置化(自动生成 route, page,被集成,但不进 git);或者通用页面,接收参数。 ======componet 设计===== 一个设计良好的组件: 1. 能简单的完成基本任务。 2. API 符合直觉。名称。 3. 不影响外部状态(样式)。 4. 错误要抛出,出错至少要对开发友好。 5. 清晰完整的使用文档。 6. 可平滑升级。 7. 如果是组件库,那么组件要有相似性,api、样式。 8. 样式 scoped 很难被外部覆盖,所以最好基于 class 的。 component 的接口: 1. 外部传进的 props,包含数据和 callback 2. component globals, $store, $http, window 3. 内部状态?只是影响组件内部逻辑是最好的。 4. 内部状态由网络获取的部分?与整体的 store 没有关系。 5. 直接发起请求影响服务器?可能影响 store 的数据一致性,那么应该有 store 去统一操作。 6. mapstate from store. component 的依赖是个多重来源的,并不由 parent component 完全控制。 7. 与 graphql 的关系 根据项目的大小划分 component 的责任: 责任列表: 1. 显示数据 2. 监听用户操作 3. 获取数据 4. 处理数据 中小项目可以将以上合在一起。大型项目中的 component 会很大很复杂。 需要将 component 的功能简化,变成只 render。 但 component 需要有办法声明数据依赖,并将对数据的影响传递出去。 container component: fetch data presentational component: display data controlled component: 由上层获取数据,数据有变化,也通过 onchange 事件通知上层去处理,或者上层传递 props 函数去处理。 container component 的子组件层级里面再包含 container component 也是允许的,只要子组件是自己管理它获取的状态就可以。 css 共享和隔离? global css, component css component 的职责:有无网络请求?有。 ======componet 设计 end===== ======脚手架设计 end======