设计是为了将困难解决在设计阶段,同时为预估工期提高准确度。
适合有点复杂度,追求高质量的项目。
设计目标:
为保证设计不快速过时,设计文档仅记录不太变动的核心数据结构和流程,部分重要细节。 某些部分记录下来,也仅仅是为了帮助初次开发,后期如果过时了,可以写明已过时不再维护了。
组件设计;仅为辅助设计的脚手架,后续不维护? 开发环境设计(基础设施、文件目录、readme):不是设计的核心内容,确实将设计实现的重要入口
预计采用的技术栈和工具链如下
// 详见:student-client.d.ts
type Store = {
/** 当前用户 */
user: {
id: number;
/** 身份证号 */
identityNumber: string;
/** 学号 */
studentCodeList: string[];
// ...
};
// ...
};
/** 得到当前客户端版本 1.9.* */
type GetCurrentClientVersion = () => string;
type IsSupportedClientVersion = () => boolean;
写明层级的目标,设计层级的协调功能(接口)。
tools layer: 工具类,可以脱离项目的函数
api layer: 通信类,处理和服务端数据的交互
service layer: 核心服务类,处理项目的生命周期
data binding layer: 提供给 UI 展示的类
user event handler layer: 提供处理用户触发事件的类
单向数据流与复式记账法的比较。
数据的处理原则要遵循以下原则:
组件的分类(适合复杂业务):
不涉及到 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 或其他事件导致要取本地存储) 组件生命周期 用户输入 原生事件(网络)
文件名/文件夹 组织模块用
强联系的文件放在一起 文件名有区分度,不要过分依赖文件路径名。否则会出现很多像是 index.js 这样的文件。 文件影响范围:不超出文件夹最好,image 或 package private
.
├── 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
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 # 显示版本号
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 的 statusCode:
api 请求是否显示错误:
noErrorMessage: boolean (某些情况下希望网络请求静默处理)
当 statusCode 为非正常时,优先显示 desc 字段,如果没有,则显示“未定义异常:......”
“websocket 重连失败” => "服务器连接失败(websocket)"
网络请求可以限流、重试、取消。
网络请求可设置遮罩层,防止请求过程中用户的误操作、重复操作等等。有 mask,拒绝键盘事件。
globalMaskCount === 0 ====> no mask
log: inc/dec globalMaskCount, reason
loading / error 状态不进 store
但是可以通过请求的状态机来处理?
const { data, isLoading, isFinished, error } = useAxios('/api/posts') 好处/难处:
network loading state from api and reactive?
get reactive network result?
form inputs / confirm / throttle? / error => network params => res/loading => display result 多个 loading 状态转换?
错误的边界是用户,无论怎么处理,错误的不可避免的,一定要通知到用户,或不给用户错误的结果(先成功后失败)。
错误分为两种,一种是给用户看的错误,一种是给程序排错或恢复的错误。
错误应该抛出 Error 对象,不能 throw 字符串等类型。
同时记录到本地日志,以及网络日志,网络日志部分可以做监控提醒。
本地日志要采用加密算法,同时在系统处于登录界面时,将本地日志上传。
日志的主要内容为:
logger => (destination: baidu / console / file / aliyun) (type: error / warning / operation) (easy / format / single / conf)
学生端是属于严格受控的页面跳转,既考虑刷新,也考虑缓存。
要禁用链接的拖动或通过按 Ctrl 点击链接多开窗口。
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:
仅有一个 MainLayout 供复用,在 router 里面设置 layout。
全局的样式包含以下功能:
组件内部的样式应采用 scoped,将样式限制在组件内起作用。
提示的种类:成功、警告、失败、进度
提示不能过于频繁,要对重复的消息提示做限流处理。
部分流程性质的消息提示,可直接更新内容,延长显示时间。比如“正在上传文件”,“上传成功”。
检查各个服务器资源的缓存设置:
service worker cache html/js/css/image 保证首页快速可用。 保证在运行过程中,可以更新到最新版本的 js。
缓存分为应用层和网络层。 应用层的缓存主要是将 store 缓存在 sessionStorage。
都要注意缓存失效的问题,比如用户点击退出回到登录界面,通过权限获取的缓存就应该失效。
PWA 最主要的功能是 cache cache 要判断:
service worker ; image cache; audio cache/prefetch
再使用了 vite 来作为开发服务器后,还有几个可以提升开发效率的功能需要研究。
http/ws proxy
开发模式下:
设置一个服务器 ip 地址,即满足所有的 https/wss 转发。
设置学校域名
设置是否为测试环境。最好能 preval,减少代码信息泄露。参考 preval.marco / ts-transformer-preval
js-obfuscator
{
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 信息
前后端协商加密解密,避免接口敏感信息被暴露。
技术难题:
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'
新增加 API 支持新前端,后端维护多套 API,一次升级。
前端保持多套,新旧共存。
nginx 通过域名来分发新旧前端代码。
如此,可支持前端灰度升级。
======测试规范===== 工具类充分测试 服务类模拟测试 前端单元测试: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======
======设计的遗留问题和感悟=====
前端架构的责任: 1. 架构是方向性的。提出目标和方向,无论底层技术和框架如何发展,目标和方向都不会变,甚至能更好的利用到变化。 2. 架构是管理变化的。当可能的变化点来临,架构能够处理。 3. 架构是划分权力和责任的。即不同的人员负责不同的模块和层级,互相有大致明确的依赖和边界。 4. 架构是负责增长的。 5. 架构是深刻理解了底层技术和业务需求,搭建出的架构既稳固又灵活。 _. 方向性:易使用、易维护、高性能、高兼容、易调试、日志完善、安全性、文档、code review。。。前端工程化 _. 日志完善 _. 作用域可控 _. 接口规范 _. 数据类型 => IDE 编程 _. 测试
设计一个项目时:
架构:关注生命周期,切割生命周期,关注模块,关注模块间的通信 工具:将常用操作收敛在一起 业务:具体细节
几种设计方式:
看得远,才能做设计。 如何看得远?经验,想象力,推理能力。
几种程序的架构方式:
前端架构:减少重复劳动。库、组件库、流程制定、高难度工具。
先开发逻辑?还是同时开发样式? 先设计通用组件?还是后面提取?
程序设计能力和过程是估工期的重要因素。
架构与收纳: 方便获取(就近) 方便记忆(分类摆放) 容易还原(将获取模式固定,避免系统状态杂乱) 流程抽象
设计程序时: 1. 分解流程(操作人的责任范围即为一个流程) 2. 制定接口(接口即为双方开发、调试时查找问题的边界) 3. 保留日志(接收的输入,返回的输出) 4. 错误处理
看源代码: 1. 模块关系 2. 模块加载 3. 模块初始化 4. 模块动态关系
架构:
框架的目的: 1. 整体可控,整体流程和接口 2. 给模块保证接口,内部可发挥,可 hack,不影响整体的质量
结构良好的程序: 1. 总组件控制流程 2. 子组件控制细节 3. 总组件和子组件通过接口来沟通 结构不好的程序: 1. 总组件只是个入口 2. 细节和跳转逻辑都在每一个子组件中
有没有可能总组件无法控制流程? 如果父子、子子组件耦合过高,那么很难梳理出清晰的总流程。
MVC 分层与组织的树状图? 组织结构 CEO 下面总监。总监下面经理。经理下面员工。 总监层? 每一个层级的职责。目标、进度、指挥、协调。
编程时脑袋里应该存放什么信息(超越本能,练习专业能力):
究竟是前端编织业务,还是后台自行沟通?
组件:
从需求推导框架的设计,最后到项目的架构: 用户需求: 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. 做好事件回调,将组件内发生的事情传递出去。
设计的原则(主要从人的大脑理解能力出发):
设计一个系统就好像给一个团队分工,系统分工不必关心具体人的能力和性格,但是需要知道底层工具组件的能力。 甚至一个系统最终是给不同的人使用的,那么这些角色的操作流程、效率、配合和权限也要考虑在系统中。
系统的演化:
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
前端开发的几大主题:
一个应用如果只考虑正确情况,那它的架构和状态应该是怎么样? 此为一个简化版本,应用的是第一性原理。 然后考虑一个应用如果考虑网络请求,网络错误,日志,性能,动画,复用,错误提示,校验?
======设计的遗留问题和感悟=====
======脚手架设计====== 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 设计===== 一个设计良好的组件:
component 的接口:
根据项目的大小划分 component 的责任: 责任列表:
中小项目可以将以上合在一起。大型项目中的 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======