关于此文档
-
一个好的文档是能够让大部分人看了就明白的,如果需要反复讲解的,一般是文档的编辑有问题。
-
大家有看不懂的或其他想法的欢迎私聊纠正,一个人往往会走入歧途,思想的火花总是在碰撞中出现。
- 不过,希望是带着解决方案提出的问题。
- 因为,往往很多问题之间都是互相矛盾的,它们不可能被完全消灭只能选择比较折中的方法
-
公共建立的初衷
- 从代码层面提高编码效率和提高代码强壮性(可维护性)
- 场景:
- 例如:过去我们要使用一个字典,总是要先定义一个空数组绑定到选择器组件上,然后在created时请求接口去初始化这个数组。往往这种接口还是允许请求多个字典的,你还得对返回的数据进行拆分
- 考虑:如果你哪天要删除这个选择器,那么你需要修改,1、删除html,2、删除数组和value绑定值,3、阅读请求接口的相关代码,拆分逻辑中的相关部分等等
- 如果:有这么一个组件,你只需要传入字典key字符串,剩下的工作组件都帮你做了,是不是能够极大得提高开发效率和代码强壮性
- 实现:把字典请求包裹到组件中,然后通过请求层的去抖动,对一个时间段内的字典请求进行合并发起,最后响应时分配响应,来同时达到组件的易用性和接口的整合。
- 总结:
- 一个项目的公共部分,它在内部要足够解耦,保证公共部分能在不同的业务场景中复用。在外部要足够方便,尽量降低业务代码编写者的难度,提高开发效率和降低维护成本。
- 它们可能是跟业务紧密关联的,例如上文中的字典组件,每个业务要用的字典值都不同
- 它们可能是需要完全脱钩的,例如上文中的字典接口去抖动,字典接口可以在任何业务下使用,对它进行去抖动都是必须且通用的
- 如何实现(只针对公共部分)
- 按周期自我审查
- 自己当场审查代码,由于熟悉度很高,是很难发现不足的。但是过一个月回头看之前写的,往往就会发现自己都读得很痛苦,就容易发现有很多需要改善的地方。
- 代码审查,让别人去看你的代码,如果很难看懂,往往代表代码组织、变量名称或注释有问题(请参考本文档第一条)
- 改自己写的代码往往都很容易,因为即使组织代码的方式犯了错,也都是自己熟悉的错。这点请参考element-ui的源码和ant-design的源码,就会发现有天壤之别。
- 保持换位思考,我开发的东西是给别人用的,如果别人开发给我用的,我希望怎么用。怎么能保证自己足够的解耦,别人使用又足够的方便
- 按周期自我审查
- 一个项目的公共部分,它在内部要足够解耦,保证公共部分能在不同的业务场景中复用。在外部要足够方便,尽量降低业务代码编写者的难度,提高开发效率和降低维护成本。
- 场景:
- 从配套工具层面提高编码效率和提高项目强壮性
- 编辑器
- vscode在不同的开发模式,需要什么插件,这些插件要如何配置
- 例如:eslint的代码检查、code spell checker的单词检查等
- vscode根据不同开发模式提供的代码片段
- vscode有哪些常用快捷键?有什么便捷操作?
- vscode在不同的开发模式,需要什么插件,这些插件要如何配置
- 打包工具
- TODO:怎么从webpack切换到vite,提高打包、热更新效率
- 怎么拆分大型项目?使用乾坤微前端?还是拆分成多项目+后端服务?
- 文档
- 如何生成公共文档?
- 如何根据外部文档生成内部代码?
- 调试
- 浏览器控制台有哪些常用调试方法?
- 如何进行性能调试?
- 测试
- 自动化测试?
- 可视化测试?
- 管理
- git仓库分支管理
- 发版合规管理
- 修改影响范围自动生成?
- 开发模板
- 应当如何组织业务代码,如何组织路由结构,才能正确显示面包屑导航。3种方案
- 如何避免多个项目间浏览器缓存的互相污染,Proxy拦截器
- 开发模式
- 公共部分和业务部分如何并行开发,即如何在业务代码中引用公共部分而不需要发布npm包
- 编辑器
- 从代码层面提高编码效率和提高代码强壮性(可维护性)
公共部分代码组织经验
-
外部包简介
- emotion
- css in js的一种实现方案,通过js代码生成css的class名(即字符串)
- 过去命名冲突和样式覆盖一直是css的痛点
- 它生成的class名是md5值解决了该问题
- 过去动态样式是通过改变绑定的class名或绑定的style来实现,需要写不同的样式来提供切换,非常繁琐
- emotion支持参数传入,参数改变,对应的样式即改变,方便实现动态样式
- 过去公共class名的使用,需要在元素的class属性上绑定多个值来实现
- emotion支持参数传入class名,直接组装多个原子类生成新的类名
- 过去class名对应的样式被删除,在运用的html中是无感知的,要检索一个class样式的使用范围是非常困难的(只能全局搜索字符串)
- emotion的class名都是引用关系,可以像模块引用那样追溯来源和使用范围
- typescript(简称ts,可选)
- 优点
- 同时维护代码、注释、文档是很反人类的,经常代码修改了,注释或文档没有跟进导致脱节
- 它可以实现代码即文档
- 例如:ts能够声明一个函数的参数是字符串类型,甚至明确到是哪几个字符串才行
const animalRun = (animal:'cat'|'dog'|'mouse')=>{}
- 例如:ts能够声明一个函数的参数是字符串类型,甚至明确到是哪几个字符串才行
- TODO:VuePress是否有现成的提取插件可以提取组件文档,有没有文档生成器可以同时支持vue、class component、composition-api?
- 它可以实现代码即文档
- js无类型的缘故,开发中可以访问一个对象的所有属性。如果代码中使用了一个库并不愿意对外暴露的属性,将导致这些库的后续升级存在隐患。
- ts限定了所有对象的输出属性,即使这个对象真实包含了该属性但类型中没有,对这些属性的访问在vscode和编译上都将会报错
- js无类型的缘故,在修改一段代码时需要了解这个代码的所有使用场景,才能够知道修改是否对原有代码造成影响(特别是有多种返回类型的函数)
- ts限定了对象的类型,一个函数返回类型和其赋值的对象的类型不一致时就会报错,能够在运行前进行检查
- 后端请求不知道返回了什么,需要查看文档
- 可以配合文档工具生成具有类型的后端接口,并保持同步,后端一旦改了返回,前端类型会立刻检查并报错
- TODO:请求提取现有的处理方案是否成熟?
- 同时维护代码、注释、文档是很反人类的,经常代码修改了,注释或文档没有跟进导致脱节
- 缺点
- 使用好的成本很高所以可选,但是强烈推荐
- 类型定义不准确会降低上面提到的优点,但是要定义非常准确的类型有时候会比写它的实现更难(例如:递归中的类型)
- 部分vue特性不好支持
- 局部指令
- 指令对于html而言就是额外的属性,ts只能全局元素声明额外属性,不能实现局部声明
- TODO:是否可以采用函数式指令?
- slots 插槽没有地方可声明
- 必须使用tsx开发,提供了vue的使用难度
- 局部指令
- 使用好的成本很高所以可选,但是强烈推荐
- 优点
- vue-tsx-support(可选,基于typescript)
- vue对于ts的支持很差,即使是用ts实现的vue3在运用时都不完善,vue-tsx-support解决了大部分问题
- 提供了 vue-router 等相关内置组件的类型
- 提供了原生dom的类型,例如 input 标签等
- 提供组件props、events和scope-slots的类型定义方案
- vue2目前提供了两种比较健全的ts支持方式,vue-tsx-support都提供了补充
- class component
- composition-api
- vue对于ts的支持很差,即使是用ts实现的vue3在运用时都不完善,vue-tsx-support解决了大部分问题
- composition-api
- vue官方提供的vue2组合式组件开发支持包,和vue3有些许区别,但是能平滑的过渡,公共代码建议都用这个开发
- 对代码组织更加合理,推荐使用,具体优点看vue3相关介绍
- emotion
-
概设
- 公共部分应该在一个git仓库中管理
- 方便开发,ui包一般需要引用utils包提供的方法,拆分不同的仓库使开发体验非常不友好
- 仓库下的每个包都会被独立发布为npm依赖,便于复用合理清依赖关系
- 例如:需要建立一个新项目,但是它不需要过去的公共组件,但是可以保留utils提供的公共方法
- 开发时在该仓库下应该针对每个包定义别名
- 别名可以模仿外部包的引用,拆分包发布时不需要再修改代码
- 别名有2个设置场景
- 非ts,比较繁琐
- vscode的别名,提供了js的跳转
- webpack的别名,提供了打包依赖加载的依据
- ts别名,只需要配置 tsconfig.json
- vscode本身支持ts别名
- babel插件tsconfig-paths-webpack-plugin,能自动同步ts别名到webpack中
- 非ts,比较繁琐
- 对于包的使用推荐使用vue-cli插件的模式
- TODO:如何开发一个vue-cli插件
- 有些私有包可以只安装依赖
- 例如:element-ui中关于组件的样式配置文件、组件类型的修复,用户只需要选择性引入需要的部分
- 有些私有包需要安装多个依赖及修改文件
- 例如:vue-tsx-support + element-ui
- 它需要引入vue-tsx-support提供的ts规则,并可被外部修改,所以只能拷贝文件到项目中
- 它需要判断是否有使用element-ui,需要按需引入element-ui提供的类型补充
- 例如:vue-tsx-support + element-ui
- 公共部分应该按使用场景拆分成独立的包,每个包只提供优化方案,最大限度保留原官方文档的通用性
- 例如:使用了element-ui这个外部库,那么就需要element-ui样式的相关配置项,所以对于它的配置对象和可响应方案应该包含到element-ui这个包中
- 可响应方案:element-ui是基于vue的,可响应方案也是基于vue的,这并不冲突
- 例如:例如el-dialog组件原生基于 fixed + vh 的方式布局,若要提供基于 flex 布局的class名,这样的样式应该归入element-ui这个包中
- 例如:使用了element-ui这个外部库,那么就需要element-ui样式的相关配置项,所以对于它的配置对象和可响应方案应该包含到element-ui这个包中
- 包
- 各个包应该提供统一的出口index文件,避免内部方法被滥用,导致包无法迭代
- 例如:element-ui内部的日期转换方法,看起来很香减少了重复代码,但实际上人家文档中并没有暴露给外部使用,万一哪天这个库更新了,这个方法被改动,将造成不可控的生产事故
- 统一出口文件应该支持树摇原则,树摇会抖掉没有被引用的模块,减小打包体积
- 各个包应该提供统一的出口index文件,避免内部方法被滥用,导致包无法迭代
- 公共部分应该在一个git仓库中管理
-
详设
-
http:请求发起对象(一般为axios,多端统一应该使用uni.app提供的api)
- 请求发起对象一般分为多种,应该区分不同的模式建立独立的实例,方便针对不同模式插入拦截器
- 例如:请求json数据的响应和请求流数据的响应很大可能不同,一般json数据会被再次包裹一层以定义与自身业务相关的状态码
- 例如:访问不同后端的接口时,不同后端模块的状态码可能不同,需要不同的拦截器进行处理
- 拦截器
- 去抖动拦截器:当一个请求未返回但又发起一个相同请求时应该拦截请求减少后端压力,并在上个请求响应时响应被拦截的发起者
- 例如:一个部门选择器初始化时需要获取部门数据,当这个组件在表格中使用时会同时发起n条部门数据请求,这些请求应该被去抖动
- 例如:一个按钮点击发起请求没有做loading效果时,用户拼命点击发起多个请求,可以在底层拦截这种错误
- 去抖动拦截器:当一个请求未返回但又发起一个相同请求时应该拦截请求减少后端压力,并在上个请求响应时响应被拦截的发起者
- 文档结构
- index.ts:提供带有默认拦截器的请求实例
- interceptors
- index.ts:提供各种拦截器
- debounce.ts:去抖动拦截器
- code.ts:状态码拦截器,应该是个状态码拦截器生成器,可以通过参数定义各种状态的处理方法
- 请求发起对象一般分为多种,应该区分不同的模式建立独立的实例,方便针对不同模式插入拦截器
-
request所有请求的集合
- 请求只和业务相关,和使用的前端框架无关,请求单独封装便于复用
- 从接口层就对数据进行处理会极大的降低组件的开发难度,还能提高使用效率
- 例如:一个部门弹窗,打开时需要初始化传入id下的部门树,组件开发时不需要考虑该id数据是否请求过是否有缓存,id是否切换是否要请求新数据,只需要打开去请求就行
- 例如:一个字典选择器,用户只需要传入一个字典key,当一个页面需要多个字典时,节流后的接口会整合多个字典值去后端请求一次,然后分派给各个独立请求发起者
- 文档结构
- original
- 后端提供的原始接口,应该有脚本自动生成和更新
- 建议统一以req开头+请求方法+该接口的后端方法名
- 根据url路径分布文件,便于查找
例如:某个接口/ucenterapi/uc/internal/department/getDepartmentListByDepartmentParentId
他的文件路径跟url一致,每个/分段是一个文件夹,最后以方法类型为文件名/ucenterapi/uc/internal/department/getDepartmentListByDepartmentParentId/get.ts
- cache
- 具有缓存特性的接口,每个接口的缓存方式不同,需要单独编写
- 例如:部门树接口、个人用户信息接口等
- 建议以original对应函数名+cache结尾
- 需要提供强制更新缓存的参数
- 主要分为
- 非数组参数缓存
- 直接判断是否有缓存
- 数组参数缓存
- 数组参数缓存先要剔除正在请求的key和已经响应的key才向后端发起
- 非数组参数缓存
- 注意
- 接口响应后,如果检查到有缓存数据了(其他接口写入的),不要替换已经存在的数据,有需要的话只进行内部更新,这样可以保证获取的引用对象是同一个
- 具有缓存特性的接口,每个接口的缓存方式不同,需要单独编写
- throttle
- 建议以original对应函数名+throttle结尾
- 具有节流特性的接口
- 例如:字典接口,在节流时间内在不同的地方使用的字典,整合成一个接口从后端获取,响应后再分派给各个请求对象
- original
-
oppein-components
- 建议统一以Op开头
- 为什么公共组件要选择typescript + composition-api + tsx
- vue单文件组件更易用,但是vue的生态是封闭的,必须有好用的vscode插件支持。vetur至今还存在格式化js代码不能直接使用eslint的问题。随着版本的迭代,这种生态健全的问题会循环出现。
- 要自动生成文档必须有对应VuePress插件,同理vue版本的迭代也会导致相同的生态问题。而ts在类型提取上具有天然优势,只要是能够提取类的属性的文档解析方案就能够使用
- tsx能够实现完全的模板编辑方式,这本来就是vue为了避免template不够全面的后备方案
- typescript更利于长期维护,vue单文件要完全支持ts,又需要vscode有相应完善的插件支持。例如:vue3在vscode中的插件volar,目前还有很多不足。
- 所以从各方面来看tsx更适合公共部分。
- 为了提高使用效率,公共组件不仅要提供视图组件还要提供业务组件
- 例如:部门选择器组件、线别选择器组件,他们的视图基本一致,但是在初始化tree数据时完全不同。
- 业务组件应该和视图组件储存在同一文件夹下
- 在文档中只需要提供视图组件的图片就能唤醒在这个组件下寻找对应的业务组件
- 只需要给自动生成的参数文档插入视图图片就能完成绝大多数的文档编写
- 关于组件开发的个人经验
-
组件的分类
- 一个组件需要从外部定义内部的状态、状态操作方法、渲染函数
- 例如:部门选择器组件还细分为局部部门选择器、全部门选择器、type是某种类型的部门选择器等
- 方案:
- 所有对于状态的操作方法及渲染方法都可以定义为prop
- 可以采用渐进式的方法,在新的业务功能出现时逐步把视图组件的内部方法改为它的prop,新的业务功能就可以通过传入prop实现
- 抛弃scope-slots,实际上它跟prop传入一个渲染函数是一个功能
- 一个组件你不使用但是需要改变它渲染的结果
- 例如:左侧菜单栏组件,业务代码往往处于菜单栏组件构成的容器中,往往不会直接使用菜单栏组件,但是在不同的业务代码中却需要改变菜单栏的渲染
- 方案:使用composition-api和单例模式配合。
- 菜单组件需要供外部修改的状态、状态操作方法、渲染函数都由composition-api返回的响应对象包裹。
- 外部可以通过修改这个对象的属性值来改变组件内部的渲染结果。
- 然后通过单例模式,使每个访问这个composition-api工厂的地方都返回同一个实例对象。
- 特点:自身是单例,跟随组件创建而创建,跟随组件销毁而销毁
- 一个组件有非常深的组件层级,但是他们都需要共用到某些状态,这些状态通过prop一层层传入是件很麻烦的事
- 例如:
- 这个场景使用的地方非常少,一个复杂组件往往由多个简单组件组成,这些简单组件都应当拓展自身的prop满足复杂组件使用
- 方案:使用composition-api和局部单例模式配合
- 如分类一中所述,实现composition-api工厂和单例模式
- 在每次工厂函数初始化时都向上层组件查找同类型的工厂返回值,当有时返回绑定在上层组件中的单例
- 直至根组件都没有时,创建单例绑定到当前组件上供后辈组件使用
- 特点:自身不是单例,当父辈级组件使用时返回单例,当父辈级组件销毁时销毁。
- 例如:
- 一个没有渲染函数的组件,实际上是一个状态和状态方法的仓库
- 例如:部门选择器需要的部门树数据,在对树数据进行遍历时需要树的二维结构,即需要根据部门树生成一个计算属性,这个计算属性将会在很多方法中被应用。这是应该把部门数据的初始化,计算属性的方法,销毁的方法集中到一个composition-api中
- 方案:
- 和深组件层级的实现方式一致,不过该单例是绑定到根组件上的,只有当根组件被销毁时才销毁
- 一个组件需要从外部定义内部的状态、状态操作方法、渲染函数
-
组件编写的一些原则
- 使用composition-api以最小粒度输出组件
- 例如:树形选择器可以根据父子是否互相关联来组织composition,关联的tree在视图显示和初始化数据时和不关联的实现方法完全不同
- 更小粒度的组件,可以减少render函数运行开销。组件当前数据不改变时,render函数是不会运行的。即父组件重新render子组件不受影响
- 为了减少数据格式转换,应该在视图组件中提供value的类型定义方法(valueIsObj,valueKeyMap)
- 例如:接口要求传递
{code:string,name:string}[]
的类型,在业务代码中不再需要转换组件返回的数据为这种格式,而是直接传递valueIsObject和valueKeyMap来实现 - 能够自动把外部数据转换为组件内数据,在emit时又转换回来,减少外部使用难度
- 例如:接口要求传递
- 使用composition-api以最小粒度输出组件
-
tsx开发经验
- ts的引入最主要是为了提供了类型支持,类型支持能够很好的声明复杂的视图组件如何去进行二次拓展,形成各种供外部使用的业务组件。
- 但是vue的tsx还不支持泛型组件,泛型组件是指一个组件可以由外部去决定内部的类型
- 例如:树形选择器组件,树的数据是由外部传入的,树数据的类型只有使用者才知道。同理对于这个数据进行处理的各种方法所接受到的参数类型也会受它影响,有了泛型组件才能够更好的约束数据和它相关prop的类型。
- 方案(感觉有待检验,有点绕不太讲得清楚)
- 简介:setup方法预期返回一个对象供render函数使用,setup本身接受props,ctx两个对象
- 在建立视图组件时同时提供一个工厂函数
- 这个工厂函数返回一个setupComposition方法,该方法作为prop供使用者传入组件内。
- 在setup中该方法会被传入props,ctx参数进行调用,其返回结果会作为setup的返回供render函数使用
- setupComposition方法调用后一般返回innerRender方法供render函数调用
- 这个工厂函数可以提供用户自定义的任何参数,来改变setupComposition方法调用后的返回结果
- 这样setupComposition方法内能包含一些共同的部分,而工厂函数又提供了自定义部分,并且工厂函数作为函数本身是支持泛型的
- 在setup中该方法会被传入props,ctx参数进行调用,其返回结果会作为setup的返回供render函数使用
- 在业务组件中,提供另一个工厂函数,它预期返回要绑定的value对象和通过调用视图组件工厂函数返回的setupComposition方法
- 在业务代码中,调用业务组件的工厂函数,获取value对象和setupComposition方法,绑定到视图组件中
-
- 文档结构
- components
- tree-multiple-dialog:树形多选弹窗组件
- Base.tsx:基础的视图组件
- department.tsx:由视图组件组成的部门树形多选弹窗组件
- line.tsx:由视图组件组成的线别树形多选弹窗组件
- tree-multiple-dialog:树形多选弹窗组件
- index.ts:统一出口
- composition:存放没有渲染函数的组合部分
- components
-
style
- 项目的样式
- 建议统一以cls开头
- 所有业务中的样式应该都提供公共方法,这个是css自身的规则,一套样式方案在多个地方使用
- TODO:需要设计规范转换为样式规范的一个过程
- 公共组件内的样式可以直接写在组件内,但尽量向公共样式靠拢
- 文档结构
- config.ts:配置项,当有设计规范时才需要,可以和element-ui包中的config.ts整合在一起
- iconfont.ts:iconfont字体相关,应该支持自动生成可传参数类型
- 当某个图标被删除时能够出现错误提醒
- TODO:SVG字体库是未来,具体使用方法?能否整合?
- index.ts:提供给外部的公共class
- element-ui.ts
- 和业务相关且和element-ui相关的class,只有当使用了element-ui库才会用到的业务范围的样式
- 项目的样式
-
utils
- index.ts:工具,未详细分类
- TODO:如果更快的索引,根据使用场景?
- Filter等
- 例如:Pagination类,用来创建Pagination组件的绑定值和提供setCurrentPage、setSize和setTotal等方法
- TODO:如果更快的索引,根据使用场景?
- index.ts:工具,未详细分类
-
typeUtils
- index.ts:类型工具,用来储存ts中的常用泛型
- 例如:DeepPartial
- 和实现紧密关联的类型工具应该和实现放在一起,例如:不同模块的请求返回的封装,应该提供一个泛型和该模块的请求封装放在同一个文件中。
- index.ts:类型工具,用来储存ts中的常用泛型
-
element-ui
- 文档结构
- index.ts
- style
- 对于样式有较多要求的项目可以考虑实现
- config.ts:样式的配置文件,可以简单的从element-ui的scss文件抽取
- 能够实现动态主题,只需要改变当前配置项
- base
- everyComponent.ts:可以根据element-ui组件分类来分类样式
- 可以修复ui库定义不够严谨的选择器,采用最小影响范围原则
- index.ts:使用emotion通过config生成基础样式,不再引入element-ui提供的css文件
- everyComponent.ts:可以根据element-ui组件分类来分类样式
- 文档结构
-
composition-api
- 执行的动作
- 安装composition-api的依赖
- 文档结构
- index.ts:按照composition-api的引入用规则提供导出项
- 执行的动作
-
vue-tsx-support
- element-ui:element-ui提供的官方@type并不健全,需要扩展
-
vue-tsx-support-vca-for-webpack4
- vue-tsx-support基于webpack5开发,其中的package部分属性和webpack4不兼容,这里进行修正方便引用
-
零散经验
js
- 函数的参数只使用一个arg对象来接收,更容易扩展,默认值在函数内的顶部解构时定义
composition-api
- setup中,第一个参数props下的属性是非响应的,要在使用它的函数作用域里解构props才能够获取到内部属性随后的改变
- 例如:
const computedData = computed(()=>{ /** 在这里面解构props,才能获取属性的随后改动 */ })
- 例如:
element-ui
- OnCheckChange,只有被渲染过的tree节点被set的时候才会触发
- 当触发项未被选中但是其子孙项被选中时childrenHasChecked返回true
- 所以需要render-after-expand=false,但是数据量大时会导致浏览器卡顿,不推荐
tsx
- TODO:原生dom和class dom都默认支持-相联的属性,不知道是不是TSX默认支持的,还是vue-tsx-support配置的
- element-ui的部分事件使用烤串命名(-),基于vue-tsx-support扩展事件类型时需要完整声明整个函数类型
- TODO:element-ui的input组件不支持v-model,有点奇怪